From 5c4ffc8a8cd880bfeefc8590f06e539c3ab1ff45 Mon Sep 17 00:00:00 2001 From: SivagurunathanV Date: Thu, 24 Jan 2019 08:00:46 -0600 Subject: [PATCH 001/686] Fix for #37739 --- .../rest/src/main/java/org/elasticsearch/client/RestClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/rest/src/main/java/org/elasticsearch/client/RestClient.java b/client/rest/src/main/java/org/elasticsearch/client/RestClient.java index 3b1946ef9ed58..c587e71973f65 100644 --- a/client/rest/src/main/java/org/elasticsearch/client/RestClient.java +++ b/client/rest/src/main/java/org/elasticsearch/client/RestClient.java @@ -398,7 +398,7 @@ static Iterable selectNodes(NodeTuple> nodeTuple, Map livingNodes = new ArrayList<>(nodeTuple.nodes.size() - blacklist.size()); + List livingNodes = new ArrayList<>(Math.max(0,nodeTuple.nodes.size() - blacklist.size())); List deadNodes = new ArrayList<>(blacklist.size()); for (Node node : nodeTuple.nodes) { DeadHostState deadness = blacklist.get(node.getHost()); From b5625fd2bb786829266ae11ccf241fd2281c63ac Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Thu, 24 Jan 2019 19:40:13 -0600 Subject: [PATCH 002/686] Adding Test Case --- .../main/java/org/elasticsearch/client/RestClient.java | 2 +- .../java/org/elasticsearch/client/RestClientTests.java | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/client/rest/src/main/java/org/elasticsearch/client/RestClient.java b/client/rest/src/main/java/org/elasticsearch/client/RestClient.java index c587e71973f65..d053bda7d44fa 100644 --- a/client/rest/src/main/java/org/elasticsearch/client/RestClient.java +++ b/client/rest/src/main/java/org/elasticsearch/client/RestClient.java @@ -398,7 +398,7 @@ static Iterable selectNodes(NodeTuple> nodeTuple, Map livingNodes = new ArrayList<>(Math.max(0,nodeTuple.nodes.size() - blacklist.size())); + List livingNodes = new ArrayList<>(Math.max(0, nodeTuple.nodes.size() - blacklist.size())); List deadNodes = new ArrayList<>(blacklist.size()); for (Node node : nodeTuple.nodes) { DeadHostState deadness = blacklist.get(node.getHost()); diff --git a/client/rest/src/test/java/org/elasticsearch/client/RestClientTests.java b/client/rest/src/test/java/org/elasticsearch/client/RestClientTests.java index 69cdfeae85dff..f3f0f0e58b98d 100644 --- a/client/rest/src/test/java/org/elasticsearch/client/RestClientTests.java +++ b/client/rest/src/test/java/org/elasticsearch/client/RestClientTests.java @@ -272,6 +272,15 @@ public String toString() { blacklist.put(n2.getHost(), new DeadHostState(new DeadHostState(timeSupplier))); blacklist.put(n3.getHost(), new DeadHostState(new DeadHostState(new DeadHostState(timeSupplier)))); + /* + * case when fewer nodeTuple than blacklist, wont result in any IllegalCapacityException + */ + { + NodeTuple> fewerNodeTuple = new NodeTuple<>(Arrays.asList(n1, n2), null); + assertSelectLivingHosts(Arrays.asList(n1), fewerNodeTuple, blacklist, NodeSelector.ANY); + assertSelectLivingHosts(Arrays.asList(n2), fewerNodeTuple, blacklist, not1); + } + /* * selectHosts will revive a single host if regardless of * blacklist time. It'll revive the node that is closest From 29eac2f7583c3fec806351dc4af9a5f605782515 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Sun, 27 Oct 2019 10:07:49 -0700 Subject: [PATCH 003/686] Pure disjunctions should rewrite to a MatchNoneQueryBuilder --- .../org/elasticsearch/index/query/BoolQueryBuilder.java | 8 +++++++- .../elasticsearch/index/query/BoolQueryBuilderTests.java | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 573fee708f20a..b75fd3ce285c3 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -418,11 +418,17 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws changed |= rewriteClauses(queryRewriteContext, filterClauses, newBuilder::filter); changed |= rewriteClauses(queryRewriteContext, shouldClauses, newBuilder::should); // lets do some early termination and prevent any kind of rewriting if we have a mandatory query that is a MatchNoneQueryBuilder - Optional any = Stream.concat(newBuilder.mustClauses.stream(), newBuilder.filterClauses.stream()) + final Stream mustAndFilterClauseStream = + Stream.concat(newBuilder.mustClauses.stream(), newBuilder.filterClauses.stream()); + Optional any = mustAndFilterClauseStream .filter(b -> b instanceof MatchNoneQueryBuilder).findAny(); if (any.isPresent()) { return any.get(); } + boolean allMatchNoneQuery = mustAndFilterClauseStream.allMatch(b -> b instanceof MatchNoneQueryBuilder); + if (allMatchNoneQuery) { + return new MatchNoneQueryBuilder(); + } if (changed) { newBuilder.adjustPureNegative = adjustPureNegative; newBuilder.minimumShouldMatch = minimumShouldMatch; diff --git a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java index 10a31d8054801..4bde1a70939f2 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java @@ -371,5 +371,11 @@ public void testRewriteWithMatchNone() throws IOException { .filter(new MatchNoneQueryBuilder())); rewritten = Rewriteable.rewrite(boolQueryBuilder, createShardContext()); assertEquals(new MatchNoneQueryBuilder(), rewritten); + + boolQueryBuilder = new BoolQueryBuilder(); + boolQueryBuilder.must(new WrapperQueryBuilder(new MatchNoneQueryBuilder().toString())); + boolQueryBuilder.filter(new WrapperQueryBuilder(new WrapperQueryBuilder(new MatchNoneQueryBuilder().toString()).toString())); + rewritten = Rewriteable.rewrite(boolQueryBuilder, createShardContext()); + assertEquals(new MatchNoneQueryBuilder(), rewritten); } } From 7cae811cc79ba3b7366d61ffa037e32ca48dbe27 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Mon, 28 Oct 2019 22:33:42 -0700 Subject: [PATCH 004/686] Handling optional clause changes as per review comments --- .../elasticsearch/index/query/BoolQueryBuilder.java | 13 +++++++------ .../index/query/BoolQueryBuilderTests.java | 3 +-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index b75fd3ce285c3..b2d602f16075d 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -418,16 +418,17 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws changed |= rewriteClauses(queryRewriteContext, filterClauses, newBuilder::filter); changed |= rewriteClauses(queryRewriteContext, shouldClauses, newBuilder::should); // lets do some early termination and prevent any kind of rewriting if we have a mandatory query that is a MatchNoneQueryBuilder - final Stream mustAndFilterClauseStream = - Stream.concat(newBuilder.mustClauses.stream(), newBuilder.filterClauses.stream()); - Optional any = mustAndFilterClauseStream + Optional any = Stream.concat(newBuilder.mustClauses.stream(), newBuilder.filterClauses.stream()) .filter(b -> b instanceof MatchNoneQueryBuilder).findAny(); if (any.isPresent()) { return any.get(); } - boolean allMatchNoneQuery = mustAndFilterClauseStream.allMatch(b -> b instanceof MatchNoneQueryBuilder); - if (allMatchNoneQuery) { - return new MatchNoneQueryBuilder(); + // early termination when must clause is empty and optional clauses is returning MatchNoneQueryBuilder + if(mustClauses.size() == 0 && filterClauses.size() == 0) { + any = newBuilder.shouldClauses.stream().filter(b -> b instanceof MatchNoneQueryBuilder).findAny(); + if (any.isPresent()) { + return any.get(); + } } if (changed) { newBuilder.adjustPureNegative = adjustPureNegative; diff --git a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java index 4bde1a70939f2..76d60ea9274d1 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java @@ -373,8 +373,7 @@ public void testRewriteWithMatchNone() throws IOException { assertEquals(new MatchNoneQueryBuilder(), rewritten); boolQueryBuilder = new BoolQueryBuilder(); - boolQueryBuilder.must(new WrapperQueryBuilder(new MatchNoneQueryBuilder().toString())); - boolQueryBuilder.filter(new WrapperQueryBuilder(new WrapperQueryBuilder(new MatchNoneQueryBuilder().toString()).toString())); + boolQueryBuilder.should(new WrapperQueryBuilder(new MatchNoneQueryBuilder().toString())); rewritten = Rewriteable.rewrite(boolQueryBuilder, createShardContext()); assertEquals(new MatchNoneQueryBuilder(), rewritten); } From 9cf3f5ac69d0d3d19192e0be74024b8b6e94ac4b Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Tue, 29 Oct 2019 07:45:03 -0700 Subject: [PATCH 005/686] All should clause MatchNone and modified Testcase --- .../index/query/BoolQueryBuilder.java | 14 +++++++------- .../index/query/BoolQueryBuilderTests.java | 6 ++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index b2d602f16075d..2f769e8097493 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -417,19 +417,19 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws changed |= rewriteClauses(queryRewriteContext, mustNotClauses, newBuilder::mustNot); changed |= rewriteClauses(queryRewriteContext, filterClauses, newBuilder::filter); changed |= rewriteClauses(queryRewriteContext, shouldClauses, newBuilder::should); + // early termination when must clause is empty and optional clauses is returning MatchNoneQueryBuilder + if(mustClauses.size() == 0 && filterClauses.size() == 0 + && newBuilder.shouldClauses.stream().allMatch(b -> b instanceof MatchNoneQueryBuilder)) { + return new MatchNoneQueryBuilder(); + } + // lets do some early termination and prevent any kind of rewriting if we have a mandatory query that is a MatchNoneQueryBuilder Optional any = Stream.concat(newBuilder.mustClauses.stream(), newBuilder.filterClauses.stream()) .filter(b -> b instanceof MatchNoneQueryBuilder).findAny(); if (any.isPresent()) { return any.get(); } - // early termination when must clause is empty and optional clauses is returning MatchNoneQueryBuilder - if(mustClauses.size() == 0 && filterClauses.size() == 0) { - any = newBuilder.shouldClauses.stream().filter(b -> b instanceof MatchNoneQueryBuilder).findAny(); - if (any.isPresent()) { - return any.get(); - } - } + if (changed) { newBuilder.adjustPureNegative = adjustPureNegative; newBuilder.minimumShouldMatch = minimumShouldMatch; diff --git a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java index 76d60ea9274d1..df0118b9992d6 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java @@ -376,5 +376,11 @@ public void testRewriteWithMatchNone() throws IOException { boolQueryBuilder.should(new WrapperQueryBuilder(new MatchNoneQueryBuilder().toString())); rewritten = Rewriteable.rewrite(boolQueryBuilder, createShardContext()); assertEquals(new MatchNoneQueryBuilder(), rewritten); + + boolQueryBuilder = new BoolQueryBuilder(); + boolQueryBuilder.should(new TermQueryBuilder("foo", "bar")); + boolQueryBuilder.should(new WrapperQueryBuilder(new MatchNoneQueryBuilder().toString())); + rewritten = Rewriteable.rewrite(boolQueryBuilder, createShardContext()); + assertNotEquals(new MatchNoneQueryBuilder(), rewritten); } } From 76287289727272e809df199cd692f27da53f7a41 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Tue, 26 Nov 2019 15:48:41 -0800 Subject: [PATCH 006/686] Adding cases for empty should clause --- .../java/org/elasticsearch/index/query/BoolQueryBuilder.java | 2 +- .../org/elasticsearch/index/query/BoolQueryBuilderTests.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 2f769e8097493..5dd7903141ddb 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -418,7 +418,7 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws changed |= rewriteClauses(queryRewriteContext, filterClauses, newBuilder::filter); changed |= rewriteClauses(queryRewriteContext, shouldClauses, newBuilder::should); // early termination when must clause is empty and optional clauses is returning MatchNoneQueryBuilder - if(mustClauses.size() == 0 && filterClauses.size() == 0 + if(mustClauses.size() == 0 && filterClauses.size() == 0 && shouldClauses.size() > 0 && newBuilder.shouldClauses.stream().allMatch(b -> b instanceof MatchNoneQueryBuilder)) { return new MatchNoneQueryBuilder(); } diff --git a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java index df0118b9992d6..1a1609fe7c50e 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java @@ -382,5 +382,9 @@ public void testRewriteWithMatchNone() throws IOException { boolQueryBuilder.should(new WrapperQueryBuilder(new MatchNoneQueryBuilder().toString())); rewritten = Rewriteable.rewrite(boolQueryBuilder, createShardContext()); assertNotEquals(new MatchNoneQueryBuilder(), rewritten); + + boolQueryBuilder = new BoolQueryBuilder(); + rewritten = Rewriteable.rewrite(boolQueryBuilder, createShardContext()); + assertNotEquals(new MatchNoneQueryBuilder(), rewritten); } } From ada16e9ecd1702c249cb1e220932251e6dbb65bb Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Sun, 8 Dec 2019 14:52:32 -0800 Subject: [PATCH 007/686] Adding best compression inital commit --- .../xpack/core/ilm/CloseIndexStep.java | 42 +++++ .../xpack/core/ilm/ForceMergeAction.java | 42 ++++- .../xpack/core/ilm/OpenIndexStep.java | 38 +++++ .../xpack/core/ilm/WaitForIndexGreenStep.java | 98 +++++++++++ .../xpack/core/ilm/CloseIndexStepTest.java | 155 +++++++++++++++++ .../xpack/core/ilm/ForceMergeActionTests.java | 77 +++++++-- .../xpack/core/ilm/OpenIndexStepTest.java | 158 ++++++++++++++++++ .../ilm/TimeseriesLifecycleTypeTests.java | 4 +- .../core/ilm/WaitForIndexGreenStepTest.java | 155 +++++++++++++++++ 9 files changed, 745 insertions(+), 24 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java new file mode 100644 index 0000000000000..8f78ce75653e5 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.close.CloseIndexRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateObserver; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.xpack.core.ilm.AsyncActionStep; + +/** + * Invokes a Close Index Step on a index. + */ +public class CloseIndexStep extends AsyncActionStep { + public static final String NAME = "close-index"; + + CloseIndexStep(StepKey key, StepKey nextStepKey, Client client) { + super(key, nextStepKey, client); + } + + @Override + public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, ClusterStateObserver observer, Listener listener) { + if(indexMetaData.getState() == IndexMetaData.State.OPEN) { + CloseIndexRequest request = new CloseIndexRequest(indexMetaData.getIndex().getName()); + getClient().admin().indices() + .close(request, ActionListener.wrap(closeIndexResponse -> listener.onResponse(true), listener::onFailure)); + } + else { + listener.onResponse(true); + } + } + + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index eb5f2b61017a1..5d4227a766f58 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -15,6 +15,8 @@ import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.codec.CodecService; +import org.elasticsearch.index.engine.EngineConfig; import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; @@ -28,42 +30,53 @@ public class ForceMergeAction implements LifecycleAction { public static final String NAME = "forcemerge"; public static final ParseField MAX_NUM_SEGMENTS_FIELD = new ParseField("max_num_segments"); + public static final ParseField BEST_COMPRESSION_FIELD = new ParseField("best_compression"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, a -> { int maxNumSegments = (int) a[0]; - return new ForceMergeAction(maxNumSegments); + boolean bestCompression = a[1] != null && (boolean) a[1]; + return new ForceMergeAction(maxNumSegments, bestCompression); }); static { PARSER.declareInt(ConstructingObjectParser.constructorArg(), MAX_NUM_SEGMENTS_FIELD); + PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), BEST_COMPRESSION_FIELD); } private final int maxNumSegments; + private final boolean bestCompression; public static ForceMergeAction parse(XContentParser parser) { return PARSER.apply(parser, null); } - public ForceMergeAction(int maxNumSegments) { + public ForceMergeAction(int maxNumSegments, boolean bestCompression) { if (maxNumSegments <= 0) { throw new IllegalArgumentException("[" + MAX_NUM_SEGMENTS_FIELD.getPreferredName() + "] must be a positive integer"); } this.maxNumSegments = maxNumSegments; + this.bestCompression = bestCompression; } public ForceMergeAction(StreamInput in) throws IOException { this.maxNumSegments = in.readVInt(); + this.bestCompression = in.readBoolean(); } public int getMaxNumSegments() { return maxNumSegments; } + public boolean isBestCompression() { + return bestCompression; + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeVInt(maxNumSegments); + out.writeBoolean(bestCompression); } @Override @@ -80,6 +93,7 @@ public boolean isSafeAction() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(MAX_NUM_SEGMENTS_FIELD.getPreferredName(), maxNumSegments); + builder.field(BEST_COMPRESSION_FIELD.getPreferredName(), bestCompression); builder.endObject(); return builder; } @@ -92,6 +106,25 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) StepKey forceMergeKey = new StepKey(phase, NAME, ForceMergeStep.NAME); StepKey countKey = new StepKey(phase, NAME, SegmentCountStep.NAME); + + if (this.bestCompression) { + StepKey closeKey = new StepKey(phase, NAME, CloseIndexStep.NAME); + StepKey openKey = new StepKey(phase, NAME, OpenIndexStep.NAME); + StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexGreenStep.NAME); + StepKey updateCompressionKey = new StepKey(phase, NAME, UpdateSettingsStep.NAME); + Settings bestCompressionSettings = Settings.builder() + .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), CodecService.BEST_COMPRESSION_CODEC).build(); + + CloseIndexStep closeIndexStep = new CloseIndexStep(closeKey, updateCompressionKey, client); + UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey, + openKey, client, bestCompressionSettings); + OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client); + WaitForIndexGreenStep waitForIndexGreenStep = new WaitForIndexGreenStep(waitForGreenIndexKey, forceMergeKey); + ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, nextStepKey, client, maxNumSegments); + return Arrays.asList(closeIndexStep, updateBestCompressionSettings, + openIndexStep, waitForIndexGreenStep, forceMergeStep); + } + UpdateSettingsStep readOnlyStep = new UpdateSettingsStep(readOnlyKey, forceMergeKey, client, readOnlySettings); ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, countKey, client, maxNumSegments); SegmentCountStep segmentCountStep = new SegmentCountStep(countKey, nextStepKey, client, maxNumSegments); @@ -100,7 +133,7 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) @Override public int hashCode() { - return Objects.hash(maxNumSegments); + return Objects.hash(maxNumSegments, bestCompression); } @Override @@ -112,7 +145,8 @@ public boolean equals(Object obj) { return false; } ForceMergeAction other = (ForceMergeAction) obj; - return Objects.equals(maxNumSegments, other.maxNumSegments); + return Objects.equals(maxNumSegments, other.maxNumSegments) + && Objects.equals(bestCompression, other.bestCompression); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java new file mode 100644 index 0000000000000..793db17eb8376 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.open.OpenIndexRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateObserver; +import org.elasticsearch.cluster.metadata.IndexMetaData; + +final class OpenIndexStep extends AsyncActionStep { + + static final String NAME = "open-index"; + + OpenIndexStep(StepKey key, StepKey nextStepKey, Client client) { + super(key, nextStepKey, client); + } + + @Override + public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, + ClusterStateObserver observer, Listener listener) { + if (indexMetaData.getState() == IndexMetaData.State.CLOSE) { + OpenIndexRequest request = new OpenIndexRequest(indexMetaData.getIndex().getName()); + getClient().admin().indices() + .open(request, + ActionListener.wrap(closeIndexResponse -> listener.onResponse(true), listener::onFailure)); + + } else { + listener.onResponse(true); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java new file mode 100644 index 0000000000000..f001644da2022 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ +package org.elasticsearch.xpack.core.ilm; + +import com.carrotsearch.hppc.cursors.IntObjectCursor; +import com.carrotsearch.hppc.cursors.ObjectCursor; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.routing.IndexRoutingTable; +import org.elasticsearch.cluster.routing.IndexShardRoutingTable; +import org.elasticsearch.cluster.routing.RoutingTable; +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.Index; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +class WaitForIndexGreenStep extends ClusterStateWaitStep { + + static final String NAME = "wait-for-index-green-step"; + + WaitForIndexGreenStep(StepKey key, StepKey nextStepKey) { + super(key, nextStepKey); + } + + @Override + public Result isConditionMet(Index index, ClusterState clusterState) { + RoutingTable routingTable = clusterState.routingTable(); + IndexRoutingTable indexRoutingTable = routingTable.index(index); + if (indexRoutingTable == null) { + return new Result(false, new Info("index is red; no IndexRoutingTable")); + } + + boolean indexIsGreen = false; + if(indexRoutingTable.allPrimaryShardsActive()) { + boolean replicaIndexIsGreen = false; + for (ObjectCursor shardRouting : indexRoutingTable.getShards().values()) { + replicaIndexIsGreen = shardRouting.value.replicaShards().stream().allMatch(ShardRouting::active); + if(!replicaIndexIsGreen) { + return new Result(false, new Info("index is yellow; not all replica shards are active")); + } + } + indexIsGreen = replicaIndexIsGreen; + } + + + if (indexIsGreen) { + return new Result(true, null); + } else { + return new Result(false, new Info("index is not green; not all shards are active")); + } + } + + static final class Info implements ToXContentObject { + + static final ParseField MESSAGE_FIELD = new ParseField("message"); + + private final String message; + + Info(String message) { + this.message = message; + } + + String getMessage() { + return message; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(MESSAGE_FIELD.getPreferredName(), message); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Info info = (Info) o; + return Objects.equals(getMessage(), info.getMessage()); + } + + @Override + public int hashCode() { + return Objects.hash(getMessage()); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java new file mode 100644 index 0000000000000..7d7a64edc6336 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.close.CloseIndexRequest; +import org.elasticsearch.action.admin.indices.close.CloseIndexResponse; +import org.elasticsearch.client.AdminClient; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.IndicesAdminClient; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.junit.Before; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import java.util.Collections; + +import static org.hamcrest.Matchers.equalTo; + +/** + * Created by sivagurunathanvelayutham on Dec, 2019 + */ +public class CloseIndexStepTest extends AbstractStepTestCase { + + private Client client; + + @Before + public void setup() { + client = Mockito.mock(Client.class); + } + + @Override + protected CloseIndexStep createRandomInstance() { + return new CloseIndexStep(randomStepKey(), randomStepKey(), client); + } + + @Override + protected CloseIndexStep mutateInstance(CloseIndexStep instance) { + Step.StepKey key = instance.getKey(); + Step.StepKey nextKey = instance.getNextStepKey(); + + switch (between(0, 1)) { + case 0: + key = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 1: + nextKey = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + + return new CloseIndexStep(key, nextKey, client); + } + + @Override + protected CloseIndexStep copyInstance(CloseIndexStep instance) { + return new CloseIndexStep(instance.getKey(), instance.getNextStepKey(), instance.getClient()); + } + + public void testPerformAction() { + IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); + + CloseIndexStep step = createRandomInstance(); + + AdminClient adminClient = Mockito.mock(AdminClient.class); + IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class); + + Mockito.when(client.admin()).thenReturn(adminClient); + Mockito.when(adminClient.indices()).thenReturn(indicesClient); + + Mockito.doAnswer((Answer) invocation -> { + CloseIndexRequest request = (CloseIndexRequest) invocation.getArguments()[0]; + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocation.getArguments()[1]; + assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()})); + listener.onResponse(new CloseIndexResponse(true, true, + Collections.singletonList(new CloseIndexResponse.IndexResult(indexMetaData.getIndex())))); + return null; + }).when(indicesClient).close(Mockito.any(), Mockito.any()); + + SetOnce actionCompleted = new SetOnce<>(); + + step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() { + + @Override + public void onResponse(boolean complete) { + actionCompleted.set(complete); + } + + @Override + public void onFailure(Exception e) { + throw new AssertionError("Unexpected method call", e); + } + }); + + assertEquals(true, actionCompleted.get()); + Mockito.verify(client, Mockito.only()).admin(); + Mockito.verify(adminClient, Mockito.only()).indices(); + Mockito.verify(indicesClient, Mockito.only()).close(Mockito.any(), Mockito.any()); + } + + + public void testPerformActionFailure() { + IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); + + CloseIndexStep step = createRandomInstance(); + Exception exception = new RuntimeException(); + AdminClient adminClient = Mockito.mock(AdminClient.class); + IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class); + + Mockito.when(client.admin()).thenReturn(adminClient); + Mockito.when(adminClient.indices()).thenReturn(indicesClient); + + Mockito.doAnswer((Answer) invocation -> { + CloseIndexRequest request = (CloseIndexRequest) invocation.getArguments()[0]; + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocation.getArguments()[1]; + assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()})); + listener.onFailure(exception); + return null; + }).when(indicesClient).close(Mockito.any(), Mockito.any()); + + SetOnce exceptionThrown = new SetOnce<>(); + + step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() { + + @Override + public void onResponse(boolean complete) { + throw new AssertionError("Unexpected method call"); + } + + @Override + public void onFailure(Exception e) { + assertSame(exception, e); + exceptionThrown.set(true); + } + }); + + assertEquals(true, exceptionThrown.get()); + Mockito.verify(client, Mockito.only()).admin(); + Mockito.verify(adminClient, Mockito.only()).indices(); + Mockito.verify(indicesClient, Mockito.only()).close(Mockito.any(), Mockito.any()); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index 92fe40a08dfcc..c12f6e7459bd2 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -13,6 +13,8 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.index.codec.CodecService; +import org.elasticsearch.index.engine.EngineConfig; import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; @@ -33,14 +35,20 @@ protected ForceMergeAction createTestInstance() { } static ForceMergeAction randomInstance() { - return new ForceMergeAction(randomIntBetween(1, 100)); + return new ForceMergeAction(randomIntBetween(1, 100), randomBoolean()); } @Override protected ForceMergeAction mutateInstance(ForceMergeAction instance) { int maxNumSegments = instance.getMaxNumSegments(); - maxNumSegments = maxNumSegments + randomIntBetween(1, 10); - return new ForceMergeAction(maxNumSegments); + boolean bestCompression = instance.isBestCompression(); + if(randomBoolean()) { + maxNumSegments = maxNumSegments + randomIntBetween(1, 10); + } + else { + bestCompression = !bestCompression; + } + return new ForceMergeAction(maxNumSegments, bestCompression); } @Override @@ -48,21 +56,7 @@ protected Reader instanceReader() { return ForceMergeAction::new; } - public void testMissingMaxNumSegments() throws IOException { - BytesReference emptyObject = BytesReference.bytes(JsonXContent.contentBuilder().startObject().endObject()); - XContentParser parser = XContentHelper.createParser(null, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, - emptyObject, XContentType.JSON); - Exception e = expectThrows(IllegalArgumentException.class, () -> ForceMergeAction.parse(parser)); - assertThat(e.getMessage(), equalTo("Required [max_num_segments]")); - } - - public void testInvalidNegativeSegmentNumber() { - Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0))); - assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer")); - } - - public void testToSteps() { - ForceMergeAction instance = createTestInstance(); + private void assertNonBestCompression(ForceMergeAction instance) { String phase = randomAlphaOfLength(5); StepKey nextStepKey = new StepKey(randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10)); List steps = instance.toSteps(null, phase, nextStepKey); @@ -79,4 +73,51 @@ public void testToSteps() { assertThat(thirdStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, SegmentCountStep.NAME))); assertThat(thirdStep.getNextStepKey(), equalTo(nextStepKey)); } + + private void assertBestCompression(ForceMergeAction instance) { + String phase = randomAlphaOfLength(5); + StepKey nextStepKey = new StepKey(randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10)); + List steps = instance.toSteps(null, phase, nextStepKey); + assertNotNull(steps); + assertEquals(5, steps.size()); + CloseIndexStep firstStep = (CloseIndexStep) steps.get(0); + UpdateSettingsStep secondStep = (UpdateSettingsStep) steps.get(1); + OpenIndexStep thirdStep = (OpenIndexStep) steps.get(2); + WaitForIndexGreenStep fourthStep = (WaitForIndexGreenStep) steps.get(3); + ForceMergeStep fifthStep = (ForceMergeStep) steps.get(4); + assertThat(firstStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, CloseIndexStep.NAME))); + assertThat(firstStep.getNextStepKey(), equalTo(secondStep.getKey())); + assertThat(secondStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, UpdateSettingsStep.NAME))); + assertThat(secondStep.getSettings().get(EngineConfig.INDEX_CODEC_SETTING.getKey()), equalTo(CodecService.BEST_COMPRESSION_CODEC)); + assertThat(secondStep.getNextStepKey(), equalTo(thirdStep.getKey())); + assertThat(thirdStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, OpenIndexStep.NAME))); + assertThat(thirdStep.getNextStepKey(), equalTo(fourthStep)); + assertThat(fourthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, WaitForIndexGreenStep.NAME))); + assertThat(fourthStep.getNextStepKey(), equalTo(fifthStep)); + assertThat(fifthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, ForceMergeStep.NAME))); + assertThat(fifthStep.getNextStepKey(), equalTo(nextStepKey)); + } + + public void testMissingMaxNumSegments() throws IOException { + BytesReference emptyObject = BytesReference.bytes(JsonXContent.contentBuilder().startObject().endObject()); + XContentParser parser = XContentHelper.createParser(null, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, + emptyObject, XContentType.JSON); + Exception e = expectThrows(IllegalArgumentException.class, () -> ForceMergeAction.parse(parser)); + assertThat(e.getMessage(), equalTo("Required [max_num_segments, best_compression]")); + } + + public void testInvalidNegativeSegmentNumber() { + Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0), false)); + assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer")); + } + + public void testToSteps() { + ForceMergeAction instance = createTestInstance(); + if (instance.isBestCompression()) { + assertBestCompression(instance); + } + else { + assertNonBestCompression(instance); + } + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java new file mode 100644 index 0000000000000..164d5f7fe4c3c --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.open.OpenIndexRequest; +import org.elasticsearch.action.admin.indices.open.OpenIndexResponse; +import org.elasticsearch.client.AdminClient; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.IndicesAdminClient; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.junit.Before; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import static org.hamcrest.Matchers.equalTo; + +/** + * Created by sivagurunathanvelayutham on Dec, 2019 + */ +public class OpenIndexStepTest extends AbstractStepTestCase { + + private Client client; + + @Before + public void setup() { + client = Mockito.mock(Client.class); + } + + @Override + protected OpenIndexStep createRandomInstance() { + return new OpenIndexStep(randomStepKey(), randomStepKey(), client); + } + + @Override + protected OpenIndexStep mutateInstance(OpenIndexStep instance) { + Step.StepKey key = instance.getKey(); + Step.StepKey nextKey = instance.getNextStepKey(); + + switch (between(0, 1)) { + case 0: + key = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 1: + nextKey = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + + return new OpenIndexStep(key, nextKey, client); + } + + @Override + protected OpenIndexStep copyInstance(OpenIndexStep instance) { + return new OpenIndexStep(instance.getKey(), instance.getNextStepKey(), instance.getClient()); + } + + public void testPerformAction() { + IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1, 5)) + .numberOfReplicas(randomIntBetween(0, 5)) + .state(IndexMetaData.State.CLOSE) + .build(); + + OpenIndexStep step = createRandomInstance(); + + AdminClient adminClient = Mockito.mock(AdminClient.class); + IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class); + + Mockito.when(client.admin()).thenReturn(adminClient); + Mockito.when(adminClient.indices()).thenReturn(indicesClient); + + Mockito.doAnswer((Answer) invocation -> { + OpenIndexRequest request = (OpenIndexRequest) invocation.getArguments()[0]; + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocation.getArguments()[1]; + assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()})); + listener.onResponse(new OpenIndexResponse(true, true)); + return null; + }).when(indicesClient).open(Mockito.any(), Mockito.any()); + + SetOnce actionCompleted = new SetOnce<>(); + + step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() { + + @Override + public void onResponse(boolean complete) { + actionCompleted.set(complete); + } + + @Override + public void onFailure(Exception e) { + throw new AssertionError("Unexpected method call", e); + } + }); + + assertEquals(true, actionCompleted.get()); + Mockito.verify(client, Mockito.only()).admin(); + Mockito.verify(adminClient, Mockito.only()).indices(); + Mockito.verify(indicesClient, Mockito.only()).open(Mockito.any(), Mockito.any()); + } + + + public void testPerformActionFailure() { + IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1, 5)) + .numberOfReplicas(randomIntBetween(0, 5)) + .state(IndexMetaData.State.CLOSE) + .build(); + + OpenIndexStep step = createRandomInstance(); + Exception exception = new RuntimeException(); + AdminClient adminClient = Mockito.mock(AdminClient.class); + IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class); + + Mockito.when(client.admin()).thenReturn(adminClient); + Mockito.when(adminClient.indices()).thenReturn(indicesClient); + + Mockito.doAnswer((Answer) invocation -> { + OpenIndexRequest request = (OpenIndexRequest) invocation.getArguments()[0]; + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocation.getArguments()[1]; + assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()})); + listener.onFailure(exception); + return null; + }).when(indicesClient).open(Mockito.any(), Mockito.any()); + + SetOnce exceptionThrown = new SetOnce<>(); + + step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() { + + @Override + public void onResponse(boolean complete) { + throw new AssertionError("Unexpected method call"); + } + + @Override + public void onFailure(Exception e) { + assertSame(exception, e); + exceptionThrown.set(true); + } + }); + + assertEquals(true, exceptionThrown.get()); + Mockito.verify(client, Mockito.only()).admin(); + Mockito.verify(adminClient, Mockito.only()).indices(); + Mockito.verify(indicesClient, Mockito.only()).open(Mockito.any(), Mockito.any()); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java index a76a22fcc7821..49bc71c55971f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java @@ -34,7 +34,7 @@ public class TimeseriesLifecycleTypeTests extends ESTestCase { private static final AllocateAction TEST_ALLOCATE_ACTION = new AllocateAction(2, Collections.singletonMap("node", "node1"),null, null); private static final DeleteAction TEST_DELETE_ACTION = new DeleteAction(); - private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1); + private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1, false); private static final RolloverAction TEST_ROLLOVER_ACTION = new RolloverAction(new ByteSizeValue(1), null, null); private static final ShrinkAction TEST_SHRINK_ACTION = new ShrinkAction(1); private static final ReadOnlyAction TEST_READ_ONLY_ACTION = new ReadOnlyAction(); @@ -492,7 +492,7 @@ private ConcurrentMap convertActionNamesToActions(Strin case DeleteAction.NAME: return new DeleteAction(); case ForceMergeAction.NAME: - return new ForceMergeAction(1); + return new ForceMergeAction(1, false); case ReadOnlyAction.NAME: return new ReadOnlyAction(); case RolloverAction.NAME: diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java new file mode 100644 index 0000000000000..ec4f73c2ec592 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.routing.IndexRoutingTable; +import org.elasticsearch.cluster.routing.RoutingTable; +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.cluster.routing.ShardRoutingState; +import org.elasticsearch.cluster.routing.TestShardRouting; +import org.elasticsearch.xpack.core.ilm.Step.StepKey; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.IsNull.notNullValue; + +public class WaitForIndexGreenStepTest extends AbstractStepTestCase { + + @Override + protected WaitForIndexGreenStep createRandomInstance() { + StepKey stepKey = randomStepKey(); + StepKey nextStepKey = randomStepKey(); + return new WaitForIndexGreenStep(stepKey, nextStepKey); + } + + @Override + protected WaitForIndexGreenStep mutateInstance(WaitForIndexGreenStep instance) { + StepKey key = instance.getKey(); + StepKey nextKey = instance.getNextStepKey(); + + if (randomBoolean()) { + key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + } else { + nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + } + + return new WaitForIndexGreenStep(key, nextKey); + } + + @Override + protected WaitForIndexGreenStep copyInstance(WaitForIndexGreenStep instance) { + return new WaitForIndexGreenStep(instance.getKey(), instance.getNextStepKey()); + } + + public void testConditionMet() { + IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(2) + .build(); + + ShardRouting shardRouting = + TestShardRouting.newShardRouting("test_index", 0, "1", true, ShardRoutingState.STARTED); + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex()) + .addShard(shardRouting).build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) + .build(); + + WaitForIndexGreenStep step = createRandomInstance(); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(true)); + assertThat(result.getInfomationContext(), nullValue()); + } + + public void testConditionNotMet() { + IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + + ShardRouting shardRouting = + TestShardRouting.newShardRouting("test_index", 0, "1", true, ShardRoutingState.INITIALIZING); + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex()) + .addShard(shardRouting).build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) + .build(); + + WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(false)); + WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + assertThat(info, notNullValue()); + assertThat(info.getMessage(), equalTo("index is not green; not all shards are active")); + } + + public void testConditionNotMetWithYellow() { + IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(2) + .build(); + + ShardRouting shardRouting = + TestShardRouting.newShardRouting("test_index", 0, "1", true, ShardRoutingState.STARTED); + + ShardRouting replicaShardRouting = + TestShardRouting.newShardRouting("test_index", 0, "2", false, ShardRoutingState.INITIALIZING); + + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex()) + .addShard(shardRouting) + .addShard(replicaShardRouting) + .build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) + .build(); + + WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(false)); + WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + assertThat(info, notNullValue()); + assertThat(info.getMessage(), equalTo("index is yellow; not all replica shards are active")); + } + + public void testConditionNotMetNoIndexRoutingTable() { + IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().build()) + .build(); + + WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(false)); + WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + assertThat(info, notNullValue()); + assertThat(info.getMessage(), equalTo("index is red; no IndexRoutingTable")); + } +} + From 7462a58a76eba1fa41c1fbb760b7071013e14168 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Sun, 8 Dec 2019 16:27:19 -0800 Subject: [PATCH 008/686] Checkstyle and License --- .../xpack/core/ilm/CloseIndexStep.java | 24 ++++++++++++++----- .../xpack/core/ilm/OpenIndexStep.java | 21 ++++++++++++---- .../xpack/core/ilm/WaitForIndexGreenStep.java | 24 ++++++++++++------- .../xpack/core/ilm/CloseIndexStepTest.java | 21 ++++++++++++---- .../xpack/core/ilm/OpenIndexStepTest.java | 21 ++++++++++++---- .../core/ilm/WaitForIndexGreenStepTest.java | 21 ++++++++++++---- 6 files changed, 98 insertions(+), 34 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java index 8f78ce75653e5..1bac9ea91813b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -1,9 +1,20 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; @@ -27,7 +38,8 @@ public class CloseIndexStep extends AsyncActionStep { } @Override - public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, ClusterStateObserver observer, Listener listener) { + public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, + ClusterStateObserver observer, Listener listener) { if(indexMetaData.getState() == IndexMetaData.State.OPEN) { CloseIndexRequest request = new CloseIndexRequest(indexMetaData.getIndex().getName()); getClient().admin().indices() diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java index 793db17eb8376..fc9cf75284411 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java @@ -1,9 +1,20 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java index f001644da2022..fc602a1a250f7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java @@ -1,13 +1,23 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; -import com.carrotsearch.hppc.cursors.IntObjectCursor; import com.carrotsearch.hppc.cursors.ObjectCursor; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.routing.IndexRoutingTable; @@ -20,9 +30,7 @@ import org.elasticsearch.index.Index; import java.io.IOException; -import java.util.List; import java.util.Objects; -import java.util.stream.Stream; class WaitForIndexGreenStep extends ClusterStateWaitStep { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java index 7d7a64edc6336..409180e2118d2 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java @@ -1,9 +1,20 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java index 164d5f7fe4c3c..95d7da6457ce8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java @@ -1,9 +1,20 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java index ec4f73c2ec592..87c1f19a56e4f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java @@ -1,9 +1,20 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; From 553451325160ba99309fd97f78ef5596a6424a81 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Sun, 8 Dec 2019 16:36:06 -0800 Subject: [PATCH 009/686] Removing Comments from file --- .../java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java | 3 --- .../org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java | 3 --- .../org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java | 3 --- 3 files changed, 9 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java index 1bac9ea91813b..53f7e469c3bbe 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -27,9 +27,6 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.xpack.core.ilm.AsyncActionStep; -/** - * Invokes a Close Index Step on a index. - */ public class CloseIndexStep extends AsyncActionStep { public static final String NAME = "close-index"; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java index 409180e2118d2..53b2c47d00b69 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java @@ -36,9 +36,6 @@ import static org.hamcrest.Matchers.equalTo; -/** - * Created by sivagurunathanvelayutham on Dec, 2019 - */ public class CloseIndexStepTest extends AbstractStepTestCase { private Client client; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java index 95d7da6457ce8..558e4ac824a11 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java @@ -34,9 +34,6 @@ import static org.hamcrest.Matchers.equalTo; -/** - * Created by sivagurunathanvelayutham on Dec, 2019 - */ public class OpenIndexStepTest extends AbstractStepTestCase { private Client client; From 25e2395eef511287e093c183a3a7540d602df2f9 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Mon, 9 Dec 2019 18:47:27 -0800 Subject: [PATCH 010/686] Added license and renamed files for tests --- .../xpack/core/ilm/CloseIndexStep.java | 19 +++-------------- .../xpack/core/ilm/OpenIndexStep.java | 20 ++++-------------- .../xpack/core/ilm/WaitForIndexGreenStep.java | 20 ++++-------------- ...StepTest.java => CloseIndexStepTests.java} | 21 ++++--------------- ...xStepTest.java => OpenIndexStepTests.java} | 21 ++++--------------- ...t.java => WaitForIndexGreenStepTests.java} | 21 ++++--------------- .../ilm/TimeSeriesLifecycleActionsIT.java | 4 ++-- 7 files changed, 25 insertions(+), 101 deletions(-) rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/{CloseIndexStepTest.java => CloseIndexStepTests.java} (87%) rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/{OpenIndexStepTest.java => OpenIndexStepTests.java} (87%) rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/{WaitForIndexGreenStepTest.java => WaitForIndexGreenStepTests.java} (88%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java index 53f7e469c3bbe..f0afab5f604ee 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ package org.elasticsearch.xpack.core.ilm; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java index fc9cf75284411..b032b0761fa44 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java @@ -1,21 +1,9 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ + package org.elasticsearch.xpack.core.ilm; import org.elasticsearch.action.ActionListener; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java index fc602a1a250f7..d717a970c66b1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java @@ -1,21 +1,9 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ + package org.elasticsearch.xpack.core.ilm; import com.carrotsearch.hppc.cursors.ObjectCursor; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java similarity index 87% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java index 53b2c47d00b69..b0980b284eb73 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ package org.elasticsearch.xpack.core.ilm; @@ -36,7 +23,7 @@ import static org.hamcrest.Matchers.equalTo; -public class CloseIndexStepTest extends AbstractStepTestCase { +public class CloseIndexStepTests extends AbstractStepTestCase { private Client client; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTests.java similarity index 87% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTests.java index 558e4ac824a11..812d90dd5c259 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTests.java @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ package org.elasticsearch.xpack.core.ilm; @@ -34,7 +21,7 @@ import static org.hamcrest.Matchers.equalTo; -public class OpenIndexStepTest extends AbstractStepTestCase { +public class OpenIndexStepTests extends AbstractStepTestCase { private Client client; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java similarity index 88% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java index 87c1f19a56e4f..46fba4bcf1770 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ package org.elasticsearch.xpack.core.ilm; @@ -36,7 +23,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; -public class WaitForIndexGreenStepTest extends AbstractStepTestCase { +public class WaitForIndexGreenStepTests extends AbstractStepTestCase { @Override protected WaitForIndexGreenStep createRandomInstance() { diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 4e160c69efd13..2ee3de617c432 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -411,7 +411,7 @@ public void testForceMergeAction() throws Exception { }; assertThat(numSegments.get(), greaterThan(1)); - createNewSingletonPolicy("warm", new ForceMergeAction(1)); + createNewSingletonPolicy("warm", new ForceMergeAction(1, false)); updatePolicy(index, policy); assertBusy(() -> { @@ -1007,7 +1007,7 @@ private void createFullPolicy(TimeValue hotTime) throws IOException { hotActions.put(RolloverAction.NAME, new RolloverAction(null, null, 1L)); Map warmActions = new HashMap<>(); warmActions.put(SetPriorityAction.NAME, new SetPriorityAction(50)); - warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1)); + warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, false)); warmActions.put(AllocateAction.NAME, new AllocateAction(1, singletonMap("_name", "integTest-1,integTest-2"), null, null)); warmActions.put(ShrinkAction.NAME, new ShrinkAction(1)); Map coldActions = new HashMap<>(); From 5d0fc47fc6cff9c78c0c867be124c7b1651d3025 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Mon, 16 Dec 2019 20:05:14 -0800 Subject: [PATCH 011/686] Modifying WaitForIndexGreen to WaitForIndexColor class --- .../xpack/core/ilm/CloseIndexStep.java | 12 +- .../xpack/core/ilm/ForceMergeAction.java | 49 +++++--- .../xpack/core/ilm/OpenIndexStep.java | 9 +- ...enStep.java => WaitForIndexColorStep.java} | 74 ++++++++++- .../xpack/core/ilm/ForceMergeActionTests.java | 28 +++-- .../ilm/TimeseriesLifecycleTypeTests.java | 5 +- ...s.java => WaitForIndexColorStepTests.java} | 115 +++++++++++++----- 7 files changed, 217 insertions(+), 75 deletions(-) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/{WaitForIndexGreenStep.java => WaitForIndexColorStep.java} (56%) rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/{WaitForIndexGreenStepTests.java => WaitForIndexColorStepTests.java} (51%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java index f0afab5f604ee..994ddf2590b58 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -12,7 +12,10 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateObserver; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.xpack.core.ilm.AsyncActionStep; + +/** + * Invokes a close step on a single index. + */ public class CloseIndexStep extends AsyncActionStep { public static final String NAME = "close-index"; @@ -24,10 +27,13 @@ public class CloseIndexStep extends AsyncActionStep { @Override public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, ClusterStateObserver observer, Listener listener) { - if(indexMetaData.getState() == IndexMetaData.State.OPEN) { + if (indexMetaData.getState() == IndexMetaData.State.OPEN) { CloseIndexRequest request = new CloseIndexRequest(indexMetaData.getIndex().getName()); getClient().admin().indices() - .close(request, ActionListener.wrap(closeIndexResponse -> listener.onResponse(true), listener::onFailure)); + .close(request, ActionListener.wrap(closeIndexResponse -> { + assert closeIndexResponse.isAcknowledged() : "close index response is not acknowledged"; + listener.onResponse(true); + }, listener::onFailure)); } else { listener.onResponse(true); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index 5d4227a766f58..636f7a83e28c8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -5,7 +5,10 @@ */ package org.elasticsearch.xpack.core.ilm; +import org.apache.lucene.codecs.Codec; +import org.elasticsearch.Version; import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; @@ -30,53 +33,61 @@ public class ForceMergeAction implements LifecycleAction { public static final String NAME = "forcemerge"; public static final ParseField MAX_NUM_SEGMENTS_FIELD = new ParseField("max_num_segments"); - public static final ParseField BEST_COMPRESSION_FIELD = new ParseField("best_compression"); + public static final ParseField CODEC = new ParseField("index.codec"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, a -> { int maxNumSegments = (int) a[0]; - boolean bestCompression = a[1] != null && (boolean) a[1]; - return new ForceMergeAction(maxNumSegments, bestCompression); + Codec codec = a[1] != null ? Codec.forName((String)a[1]): Codec.getDefault(); + return new ForceMergeAction(maxNumSegments, codec); }); static { PARSER.declareInt(ConstructingObjectParser.constructorArg(), MAX_NUM_SEGMENTS_FIELD); - PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), BEST_COMPRESSION_FIELD); + PARSER.declareInt(ConstructingObjectParser.constructorArg(), CODEC); } private final int maxNumSegments; - private final boolean bestCompression; + private final Codec codec; public static ForceMergeAction parse(XContentParser parser) { return PARSER.apply(parser, null); } - public ForceMergeAction(int maxNumSegments, boolean bestCompression) { + public ForceMergeAction(int maxNumSegments, Codec codec) { if (maxNumSegments <= 0) { throw new IllegalArgumentException("[" + MAX_NUM_SEGMENTS_FIELD.getPreferredName() + "] must be a positive integer"); } this.maxNumSegments = maxNumSegments; - this.bestCompression = bestCompression; + this.codec = codec; } public ForceMergeAction(StreamInput in) throws IOException { this.maxNumSegments = in.readVInt(); - this.bestCompression = in.readBoolean(); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + this.codec = Codec.forName(in.readString()); + } else { + this.codec = Codec.getDefault(); + } } public int getMaxNumSegments() { return maxNumSegments; } - public boolean isBestCompression() { - return bestCompression; + public Codec getCodec() { + return this.codec; } @Override public void writeTo(StreamOutput out) throws IOException { out.writeVInt(maxNumSegments); - out.writeBoolean(bestCompression); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeString(codec.getName()); + } else { + out.writeString(Codec.getDefault().getName()); + } } @Override @@ -93,7 +104,7 @@ public boolean isSafeAction() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(MAX_NUM_SEGMENTS_FIELD.getPreferredName(), maxNumSegments); - builder.field(BEST_COMPRESSION_FIELD.getPreferredName(), bestCompression); + builder.field(CODEC.getPreferredName(), codec); builder.endObject(); return builder; } @@ -101,25 +112,25 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override public List toSteps(Client client, String phase, Step.StepKey nextStepKey) { Settings readOnlySettings = Settings.builder().put(IndexMetaData.SETTING_BLOCKS_WRITE, true).build(); + Settings bestCompressionSettings = Settings.builder() + .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), CodecService.BEST_COMPRESSION_CODEC).build(); StepKey readOnlyKey = new StepKey(phase, NAME, ReadOnlyAction.NAME); StepKey forceMergeKey = new StepKey(phase, NAME, ForceMergeStep.NAME); StepKey countKey = new StepKey(phase, NAME, SegmentCountStep.NAME); - if (this.bestCompression) { + if (codec.getName().equals(CodecService.BEST_COMPRESSION_CODEC)) { StepKey closeKey = new StepKey(phase, NAME, CloseIndexStep.NAME); StepKey openKey = new StepKey(phase, NAME, OpenIndexStep.NAME); - StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexGreenStep.NAME); + StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexColorStep.NAME); StepKey updateCompressionKey = new StepKey(phase, NAME, UpdateSettingsStep.NAME); - Settings bestCompressionSettings = Settings.builder() - .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), CodecService.BEST_COMPRESSION_CODEC).build(); CloseIndexStep closeIndexStep = new CloseIndexStep(closeKey, updateCompressionKey, client); UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey, openKey, client, bestCompressionSettings); OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client); - WaitForIndexGreenStep waitForIndexGreenStep = new WaitForIndexGreenStep(waitForGreenIndexKey, forceMergeKey); + WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey, forceMergeKey, ClusterHealthStatus.GREEN); ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, nextStepKey, client, maxNumSegments); return Arrays.asList(closeIndexStep, updateBestCompressionSettings, openIndexStep, waitForIndexGreenStep, forceMergeStep); @@ -133,7 +144,7 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) @Override public int hashCode() { - return Objects.hash(maxNumSegments, bestCompression); + return Objects.hash(maxNumSegments, codec); } @Override @@ -146,7 +157,7 @@ public boolean equals(Object obj) { } ForceMergeAction other = (ForceMergeAction) obj; return Objects.equals(maxNumSegments, other.maxNumSegments) - && Objects.equals(bestCompression, other.bestCompression); + && Objects.equals(codec, other.codec); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java index b032b0761fa44..907750e212332 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java @@ -13,6 +13,10 @@ import org.elasticsearch.cluster.ClusterStateObserver; import org.elasticsearch.cluster.metadata.IndexMetaData; +/** + * Invokes a open step on a single index. + */ + final class OpenIndexStep extends AsyncActionStep { static final String NAME = "open-index"; @@ -28,7 +32,10 @@ public void performAction(IndexMetaData indexMetaData, ClusterState currentClust OpenIndexRequest request = new OpenIndexRequest(indexMetaData.getIndex().getName()); getClient().admin().indices() .open(request, - ActionListener.wrap(closeIndexResponse -> listener.onResponse(true), listener::onFailure)); + ActionListener.wrap(openIndexResponse-> { + assert openIndexResponse.isAcknowledged() : "open Index response is not acknowledged"; + listener.onResponse(true); + }, listener::onFailure)); } else { listener.onResponse(true); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java similarity index 56% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java index d717a970c66b1..812ad66323668 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java @@ -8,6 +8,7 @@ import com.carrotsearch.hppc.cursors.ObjectCursor; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.routing.IndexRoutingTable; import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.RoutingTable; @@ -20,28 +21,91 @@ import java.io.IOException; import java.util.Objects; -class WaitForIndexGreenStep extends ClusterStateWaitStep { +/** + * Wait Step for index based on color + * */ - static final String NAME = "wait-for-index-green-step"; +class WaitForIndexColorStep extends ClusterStateWaitStep { - WaitForIndexGreenStep(StepKey key, StepKey nextStepKey) { + static final String NAME = "wait-for-index-color-step"; + + private final ClusterHealthStatus color; + + WaitForIndexColorStep(StepKey key, StepKey nextStepKey, ClusterHealthStatus color) { super(key, nextStepKey); + this.color = color; + } + + public ClusterHealthStatus getColor() { + return this.color; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), this.color); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (!(obj instanceof WaitForIndexColorStep)) return false; + WaitForIndexColorStep other = (WaitForIndexColorStep) obj; + return super.equals(obj) && Objects.equals(this.color, other.color); } @Override public Result isConditionMet(Index index, ClusterState clusterState) { RoutingTable routingTable = clusterState.routingTable(); IndexRoutingTable indexRoutingTable = routingTable.index(index); + Result result; + switch (this.color) { + case GREEN: + result = waitForGreen(indexRoutingTable); + break; + case YELLOW: + result = waitForYellow(indexRoutingTable); + break; + case RED: + result = waitForRed(indexRoutingTable); + break; + default: + result = new Result(false, new Info("No index color match")); + break; + } + return result; + } + + private Result waitForRed(IndexRoutingTable indexRoutingTable) { + if (indexRoutingTable == null) { + return new Result(true, new Info("Index is red")); + } + return new Result(false, new Info("Index is not red")); + } + + private Result waitForYellow(IndexRoutingTable indexRoutingTable) { + if (indexRoutingTable == null) { + return new Result(false, new Info("index is red; no IndexRoutingTable")); + } + + boolean indexIsAtLeastYellow = indexRoutingTable.allPrimaryShardsActive(); + if (indexIsAtLeastYellow) { + return new Result(true, null); + } else { + return new Result(false, new Info("index is red; not all primary shards are active")); + } + } + + private Result waitForGreen(IndexRoutingTable indexRoutingTable) { if (indexRoutingTable == null) { return new Result(false, new Info("index is red; no IndexRoutingTable")); } boolean indexIsGreen = false; - if(indexRoutingTable.allPrimaryShardsActive()) { + if (indexRoutingTable.allPrimaryShardsActive()) { boolean replicaIndexIsGreen = false; for (ObjectCursor shardRouting : indexRoutingTable.getShards().values()) { replicaIndexIsGreen = shardRouting.value.replicaShards().stream().allMatch(ShardRouting::active); - if(!replicaIndexIsGreen) { + if (!replicaIndexIsGreen) { return new Result(false, new Info("index is yellow; not all replica shards are active")); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index c12f6e7459bd2..4f899f3b74355 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ilm; +import org.apache.lucene.codecs.Codec; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.Writeable.Reader; @@ -35,20 +36,21 @@ protected ForceMergeAction createTestInstance() { } static ForceMergeAction randomInstance() { - return new ForceMergeAction(randomIntBetween(1, 100), randomBoolean()); + return new ForceMergeAction(randomIntBetween(1, 100), createRandomCompressionSettings()); + } + + static Codec createRandomCompressionSettings() { + if(randomBoolean()) { + return Codec.getDefault(); + } + return Codec.forName(CodecService.BEST_COMPRESSION_CODEC); } @Override protected ForceMergeAction mutateInstance(ForceMergeAction instance) { int maxNumSegments = instance.getMaxNumSegments(); - boolean bestCompression = instance.isBestCompression(); - if(randomBoolean()) { - maxNumSegments = maxNumSegments + randomIntBetween(1, 10); - } - else { - bestCompression = !bestCompression; - } - return new ForceMergeAction(maxNumSegments, bestCompression); + maxNumSegments = maxNumSegments + randomIntBetween(1, 10); + return new ForceMergeAction(maxNumSegments, Codec.getDefault()); } @Override @@ -83,7 +85,7 @@ private void assertBestCompression(ForceMergeAction instance) { CloseIndexStep firstStep = (CloseIndexStep) steps.get(0); UpdateSettingsStep secondStep = (UpdateSettingsStep) steps.get(1); OpenIndexStep thirdStep = (OpenIndexStep) steps.get(2); - WaitForIndexGreenStep fourthStep = (WaitForIndexGreenStep) steps.get(3); + WaitForIndexColorStep fourthStep = (WaitForIndexColorStep) steps.get(3); ForceMergeStep fifthStep = (ForceMergeStep) steps.get(4); assertThat(firstStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, CloseIndexStep.NAME))); assertThat(firstStep.getNextStepKey(), equalTo(secondStep.getKey())); @@ -92,7 +94,7 @@ private void assertBestCompression(ForceMergeAction instance) { assertThat(secondStep.getNextStepKey(), equalTo(thirdStep.getKey())); assertThat(thirdStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, OpenIndexStep.NAME))); assertThat(thirdStep.getNextStepKey(), equalTo(fourthStep)); - assertThat(fourthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, WaitForIndexGreenStep.NAME))); + assertThat(fourthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, WaitForIndexColorStep.NAME))); assertThat(fourthStep.getNextStepKey(), equalTo(fifthStep)); assertThat(fifthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, ForceMergeStep.NAME))); assertThat(fifthStep.getNextStepKey(), equalTo(nextStepKey)); @@ -107,13 +109,13 @@ public void testMissingMaxNumSegments() throws IOException { } public void testInvalidNegativeSegmentNumber() { - Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0), false)); + Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0), Codec.getDefault())); assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer")); } public void testToSteps() { ForceMergeAction instance = createTestInstance(); - if (instance.isBestCompression()) { + if (CodecService.BEST_COMPRESSION_CODEC.equals(instance.getCodec().getName())) { assertBestCompression(instance); } else { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java index 49bc71c55971f..1469e40e6682d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ilm; +import org.apache.lucene.codecs.Codec; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; @@ -34,7 +35,7 @@ public class TimeseriesLifecycleTypeTests extends ESTestCase { private static final AllocateAction TEST_ALLOCATE_ACTION = new AllocateAction(2, Collections.singletonMap("node", "node1"),null, null); private static final DeleteAction TEST_DELETE_ACTION = new DeleteAction(); - private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1, false); + private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1, Codec.getDefault()); private static final RolloverAction TEST_ROLLOVER_ACTION = new RolloverAction(new ByteSizeValue(1), null, null); private static final ShrinkAction TEST_SHRINK_ACTION = new ShrinkAction(1); private static final ReadOnlyAction TEST_READ_ONLY_ACTION = new ReadOnlyAction(); @@ -492,7 +493,7 @@ private ConcurrentMap convertActionNamesToActions(Strin case DeleteAction.NAME: return new DeleteAction(); case ForceMergeAction.NAME: - return new ForceMergeAction(1, false); + return new ForceMergeAction(1, Codec.getDefault()); case ReadOnlyAction.NAME: return new ReadOnlyAction(); case RolloverAction.NAME: diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java similarity index 51% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java index 46fba4bcf1770..0baa5a830df82 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.routing.IndexRoutingTable; @@ -23,35 +24,48 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; -public class WaitForIndexGreenStepTests extends AbstractStepTestCase { +public class WaitForIndexColorStepTests extends AbstractStepTestCase { + + private static ClusterHealthStatus randomColor() { + String[] colors = new String[]{"green", "yellow", "red"}; + int randomColor = randomIntBetween(0, colors.length - 1); + return ClusterHealthStatus.fromString(colors[randomColor]); + } @Override - protected WaitForIndexGreenStep createRandomInstance() { + protected WaitForIndexColorStep createRandomInstance() { StepKey stepKey = randomStepKey(); StepKey nextStepKey = randomStepKey(); - return new WaitForIndexGreenStep(stepKey, nextStepKey); + ClusterHealthStatus color = randomColor(); + return new WaitForIndexColorStep(stepKey, nextStepKey, color); } @Override - protected WaitForIndexGreenStep mutateInstance(WaitForIndexGreenStep instance) { + protected WaitForIndexColorStep mutateInstance(WaitForIndexColorStep instance) { StepKey key = instance.getKey(); StepKey nextKey = instance.getNextStepKey(); - - if (randomBoolean()) { - key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); - } else { - nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + ClusterHealthStatus color = instance.getColor(); + + switch (between(0, 2)) { + case 0: + key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 1: + nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 2: + color = randomColor(); } - return new WaitForIndexGreenStep(key, nextKey); + return new WaitForIndexColorStep(key, nextKey, color); } @Override - protected WaitForIndexGreenStep copyInstance(WaitForIndexGreenStep instance) { - return new WaitForIndexGreenStep(instance.getKey(), instance.getNextStepKey()); + protected WaitForIndexColorStep copyInstance(WaitForIndexColorStep instance) { + return new WaitForIndexColorStep(instance.getKey(), instance.getNextStepKey(), instance.getColor()); } - public void testConditionMet() { + public void testConditionMetForGreen() { IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) .settings(settings(Version.CURRENT)) .numberOfShards(1) @@ -68,13 +82,13 @@ public void testConditionMet() { .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) .build(); - WaitForIndexGreenStep step = createRandomInstance(); + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); assertThat(result.isComplete(), is(true)); assertThat(result.getInfomationContext(), nullValue()); } - public void testConditionNotMet() { + public void testConditionNotMetForGreen() { IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) .settings(settings(Version.CURRENT)) .numberOfShards(1) @@ -91,47 +105,84 @@ public void testConditionNotMet() { .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) .build(); - WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); assertThat(result.isComplete(), is(false)); - WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext(); assertThat(info, notNullValue()); assertThat(info.getMessage(), equalTo("index is not green; not all shards are active")); } - public void testConditionNotMetWithYellow() { + public void testConditionNotMetNoIndexRoutingTable() { IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) .settings(settings(Version.CURRENT)) .numberOfShards(1) - .numberOfReplicas(2) + .numberOfReplicas(0) .build(); - ShardRouting shardRouting = - TestShardRouting.newShardRouting("test_index", 0, "1", true, ShardRoutingState.STARTED); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().build()) + .build(); - ShardRouting replicaShardRouting = - TestShardRouting.newShardRouting("test_index", 0, "2", false, ShardRoutingState.INITIALIZING); + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(false)); + WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext(); + assertThat(info, notNullValue()); + assertThat(info.getMessage(), equalTo("index is red; no IndexRoutingTable")); + } + + public void testConditionMetForYellow() { + IndexMetaData indexMetadata = IndexMetaData.builder("former-follower-index") + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + ShardRouting shardRouting = + TestShardRouting.newShardRouting("index2", 0, "1", true, ShardRoutingState.STARTED); IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex()) - .addShard(shardRouting) - .addShard(replicaShardRouting) + .addShard(shardRouting).build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) + .build(); + + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(true)); + assertThat(result.getInfomationContext(), nullValue()); + } + + public void testConditionNotMetForYellow() { + IndexMetaData indexMetadata = IndexMetaData.builder("former-follower-index") + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) .build(); + ShardRouting shardRouting = + TestShardRouting.newShardRouting("index2", 0, "1", true, ShardRoutingState.INITIALIZING); + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex()) + .addShard(shardRouting).build(); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) .metaData(MetaData.builder().put(indexMetadata, true).build()) .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) .build(); - WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); assertThat(result.isComplete(), is(false)); - WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext(); assertThat(info, notNullValue()); - assertThat(info.getMessage(), equalTo("index is yellow; not all replica shards are active")); + assertThat(info.getMessage(), equalTo("index is red; not all primary shards are active")); } - public void testConditionNotMetNoIndexRoutingTable() { - IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) + public void testConditionNotMetNoIndexRoutingTableForYellow() { + IndexMetaData indexMetadata = IndexMetaData.builder("former-follower-index") .settings(settings(Version.CURRENT)) .numberOfShards(1) .numberOfReplicas(0) @@ -142,10 +193,10 @@ public void testConditionNotMetNoIndexRoutingTable() { .routingTable(RoutingTable.builder().build()) .build(); - WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); assertThat(result.isComplete(), is(false)); - WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext(); assertThat(info, notNullValue()); assertThat(info.getMessage(), equalTo("index is red; no IndexRoutingTable")); } From 69a633edeb46e97b21c4338ccf2f49d549f7ab52 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Mon, 16 Dec 2019 20:34:58 -0800 Subject: [PATCH 012/686] Precommit check changes --- .../org/elasticsearch/xpack/core/ilm/ForceMergeAction.java | 7 ++++--- .../xpack/core/ilm/ForceMergeActionTests.java | 3 ++- .../xpack/ilm/TimeSeriesLifecycleActionsIT.java | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index 636f7a83e28c8..ef49d537684a8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -38,7 +38,7 @@ public class ForceMergeAction implements LifecycleAction { private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, a -> { int maxNumSegments = (int) a[0]; - Codec codec = a[1] != null ? Codec.forName((String)a[1]): Codec.getDefault(); + Codec codec = a[1] != null ? Codec.forName((String) a[1]) : Codec.getDefault(); return new ForceMergeAction(maxNumSegments, codec); }); @@ -68,7 +68,7 @@ public ForceMergeAction(StreamInput in) throws IOException { if (in.getVersion().onOrAfter(Version.V_8_0_0)) { this.codec = Codec.forName(in.readString()); } else { - this.codec = Codec.getDefault(); + this.codec = Codec.getDefault(); } } @@ -130,7 +130,8 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey, openKey, client, bestCompressionSettings); OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client); - WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey, forceMergeKey, ClusterHealthStatus.GREEN); + WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey, + forceMergeKey, ClusterHealthStatus.GREEN); ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, nextStepKey, client, maxNumSegments); return Arrays.asList(closeIndexStep, updateBestCompressionSettings, openIndexStep, waitForIndexGreenStep, forceMergeStep); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index 4f899f3b74355..ef88e461c0d13 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -109,7 +109,8 @@ public void testMissingMaxNumSegments() throws IOException { } public void testInvalidNegativeSegmentNumber() { - Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0), Codec.getDefault())); + Exception r = expectThrows(IllegalArgumentException.class, () -> new + ForceMergeAction(randomIntBetween(-10, 0), Codec.getDefault())); assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer")); } diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 2ee3de617c432..f1de3fe738820 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -10,6 +10,7 @@ import org.apache.http.entity.StringEntity; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; +import org.apache.lucene.codecs.Codec; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; @@ -411,7 +412,7 @@ public void testForceMergeAction() throws Exception { }; assertThat(numSegments.get(), greaterThan(1)); - createNewSingletonPolicy("warm", new ForceMergeAction(1, false)); + createNewSingletonPolicy("warm", new ForceMergeAction(1, Codec.getDefault())); updatePolicy(index, policy); assertBusy(() -> { @@ -1007,7 +1008,7 @@ private void createFullPolicy(TimeValue hotTime) throws IOException { hotActions.put(RolloverAction.NAME, new RolloverAction(null, null, 1L)); Map warmActions = new HashMap<>(); warmActions.put(SetPriorityAction.NAME, new SetPriorityAction(50)); - warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, false)); + warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, Codec.getDefault())); warmActions.put(AllocateAction.NAME, new AllocateAction(1, singletonMap("_name", "integTest-1,integTest-2"), null, null)); warmActions.put(ShrinkAction.NAME, new ShrinkAction(1)); Map coldActions = new HashMap<>(); From c28a39fcfc03131b81efa0a83b90fec142d4f981 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Tue, 17 Dec 2019 21:26:29 -0800 Subject: [PATCH 013/686] Naming change --- .../java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index ef49d537684a8..78417857a7363 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -33,7 +33,7 @@ public class ForceMergeAction implements LifecycleAction { public static final String NAME = "forcemerge"; public static final ParseField MAX_NUM_SEGMENTS_FIELD = new ParseField("max_num_segments"); - public static final ParseField CODEC = new ParseField("index.codec"); + public static final ParseField CODEC = new ParseField("index_codec"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, a -> { From 8b9f86f586cfa76a8df7bea6c56fde28318d46bc Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Wed, 18 Dec 2019 23:18:11 -0800 Subject: [PATCH 014/686] Review comments by dakrone --- .../xpack/core/ilm/ForceMergeAction.java | 68 +++++++++++-------- .../xpack/core/ilm/WaitForIndexColorStep.java | 43 ++++++------ .../xpack/core/ilm/ForceMergeActionTests.java | 19 ++++-- .../ilm/TimeseriesLifecycleTypeTests.java | 2 +- 4 files changed, 76 insertions(+), 56 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index 78417857a7363..6ee7c3eb2df19 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -10,6 +10,7 @@ import org.elasticsearch.client.Client; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -23,7 +24,7 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -38,13 +39,13 @@ public class ForceMergeAction implements LifecycleAction { private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, a -> { int maxNumSegments = (int) a[0]; - Codec codec = a[1] != null ? Codec.forName((String) a[1]) : Codec.getDefault(); + Codec codec = a[1] != null ? Codec.forName((String) a[1]) : null; return new ForceMergeAction(maxNumSegments, codec); }); static { PARSER.declareInt(ConstructingObjectParser.constructorArg(), MAX_NUM_SEGMENTS_FIELD); - PARSER.declareInt(ConstructingObjectParser.constructorArg(), CODEC); + PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), CODEC); } private final int maxNumSegments; @@ -54,21 +55,24 @@ public static ForceMergeAction parse(XContentParser parser) { return PARSER.apply(parser, null); } - public ForceMergeAction(int maxNumSegments, Codec codec) { + public ForceMergeAction(int maxNumSegments, @Nullable Codec codec) { if (maxNumSegments <= 0) { throw new IllegalArgumentException("[" + MAX_NUM_SEGMENTS_FIELD.getPreferredName() + "] must be a positive integer"); } this.maxNumSegments = maxNumSegments; + if (codec != null && Codec.forName(codec.getName()) == null) { + throw new IllegalArgumentException("Compression type of " + codec.getName() + "does not exist"); + } this.codec = codec; } public ForceMergeAction(StreamInput in) throws IOException { this.maxNumSegments = in.readVInt(); if (in.getVersion().onOrAfter(Version.V_8_0_0)) { - this.codec = Codec.forName(in.readString()); + this.codec = Codec.forName(in.readOptionalString()); } else { - this.codec = Codec.getDefault(); + this.codec = null; } } @@ -84,9 +88,9 @@ public Codec getCodec() { public void writeTo(StreamOutput out) throws IOException { out.writeVInt(maxNumSegments); if (out.getVersion().onOrAfter(Version.V_8_0_0)) { - out.writeString(codec.getName()); + out.writeOptionalString(codec.getName()); } else { - out.writeString(Codec.getDefault().getName()); + out.writeString(null); } } @@ -104,7 +108,9 @@ public boolean isSafeAction() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(MAX_NUM_SEGMENTS_FIELD.getPreferredName(), maxNumSegments); - builder.field(CODEC.getPreferredName(), codec); + if (codec != null) { + builder.field(CODEC.getPreferredName(), codec); + } builder.endObject(); return builder; } @@ -119,28 +125,34 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) StepKey forceMergeKey = new StepKey(phase, NAME, ForceMergeStep.NAME); StepKey countKey = new StepKey(phase, NAME, SegmentCountStep.NAME); - - if (codec.getName().equals(CodecService.BEST_COMPRESSION_CODEC)) { - StepKey closeKey = new StepKey(phase, NAME, CloseIndexStep.NAME); - StepKey openKey = new StepKey(phase, NAME, OpenIndexStep.NAME); - StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexColorStep.NAME); - StepKey updateCompressionKey = new StepKey(phase, NAME, UpdateSettingsStep.NAME); - - CloseIndexStep closeIndexStep = new CloseIndexStep(closeKey, updateCompressionKey, client); - UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey, - openKey, client, bestCompressionSettings); - OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client); - WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey, - forceMergeKey, ClusterHealthStatus.GREEN); - ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, nextStepKey, client, maxNumSegments); - return Arrays.asList(closeIndexStep, updateBestCompressionSettings, - openIndexStep, waitForIndexGreenStep, forceMergeStep); - } + StepKey closeKey = new StepKey(phase, NAME, CloseIndexStep.NAME); + StepKey openKey = new StepKey(phase, NAME, OpenIndexStep.NAME); + StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexColorStep.NAME); + StepKey updateCompressionKey = new StepKey(phase, NAME, UpdateSettingsStep.NAME); UpdateSettingsStep readOnlyStep = new UpdateSettingsStep(readOnlyKey, forceMergeKey, client, readOnlySettings); ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, countKey, client, maxNumSegments); SegmentCountStep segmentCountStep = new SegmentCountStep(countKey, nextStepKey, client, maxNumSegments); - return Arrays.asList(readOnlyStep, forceMergeStep, segmentCountStep); + CloseIndexStep closeIndexStep = new CloseIndexStep(closeKey, updateCompressionKey, client); + UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey, + openKey, client, bestCompressionSettings); + OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client); + WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey, + forceMergeKey, ClusterHealthStatus.GREEN); + + List mergeSteps = new ArrayList<>(); + mergeSteps.add(readOnlyStep); + + if (codec.getName().equals(CodecService.BEST_COMPRESSION_CODEC)) { + mergeSteps.add(closeIndexStep); + mergeSteps.add(updateBestCompressionSettings); + mergeSteps.add(openIndexStep); + mergeSteps.add(waitForIndexGreenStep); + } + + mergeSteps.add(forceMergeStep); + mergeSteps.add(segmentCountStep); + return mergeSteps; } @Override @@ -158,7 +170,7 @@ public boolean equals(Object obj) { } ForceMergeAction other = (ForceMergeAction) obj; return Objects.equals(maxNumSegments, other.maxNumSegments) - && Objects.equals(codec, other.codec); + && ((codec == null && other.codec == null) || Objects.equals(codec, other.codec)); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java index 812ad66323668..bfaf6ab5bfcb1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java @@ -23,7 +23,7 @@ /** * Wait Step for index based on color - * */ + */ class WaitForIndexColorStep extends ClusterStateWaitStep { @@ -47,8 +47,12 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (obj == null) return false; - if (!(obj instanceof WaitForIndexColorStep)) return false; + if (obj == null) { + return false; + } + if (obj.getClass() != getClass()) { + return false; + } WaitForIndexColorStep other = (WaitForIndexColorStep) obj; return super.equals(obj) && Objects.equals(this.color, other.color); } @@ -69,7 +73,7 @@ public Result isConditionMet(Index index, ClusterState clusterState) { result = waitForRed(indexRoutingTable); break; default: - result = new Result(false, new Info("No index color match")); + result = new Result(false, new Info("no index color match")); break; } return result; @@ -77,14 +81,14 @@ public Result isConditionMet(Index index, ClusterState clusterState) { private Result waitForRed(IndexRoutingTable indexRoutingTable) { if (indexRoutingTable == null) { - return new Result(true, new Info("Index is red")); + return new Result(true, new Info("index is red")); } - return new Result(false, new Info("Index is not red")); + return new Result(false, new Info("index is not red")); } private Result waitForYellow(IndexRoutingTable indexRoutingTable) { if (indexRoutingTable == null) { - return new Result(false, new Info("index is red; no IndexRoutingTable")); + return new Result(false, new Info("index is red; no indexRoutingTable")); } boolean indexIsAtLeastYellow = indexRoutingTable.allPrimaryShardsActive(); @@ -97,27 +101,20 @@ private Result waitForYellow(IndexRoutingTable indexRoutingTable) { private Result waitForGreen(IndexRoutingTable indexRoutingTable) { if (indexRoutingTable == null) { - return new Result(false, new Info("index is red; no IndexRoutingTable")); + return new Result(false, new Info("index is red; no indexRoutingTable")); } - boolean indexIsGreen = false; if (indexRoutingTable.allPrimaryShardsActive()) { - boolean replicaIndexIsGreen = false; for (ObjectCursor shardRouting : indexRoutingTable.getShards().values()) { - replicaIndexIsGreen = shardRouting.value.replicaShards().stream().allMatch(ShardRouting::active); - if (!replicaIndexIsGreen) { + boolean replicaIndexIsGreen = shardRouting.value.replicaShards().stream().allMatch(ShardRouting::active); + if (replicaIndexIsGreen == false) { return new Result(false, new Info("index is yellow; not all replica shards are active")); } } - indexIsGreen = replicaIndexIsGreen; - } - - - if (indexIsGreen) { return new Result(true, null); - } else { - return new Result(false, new Info("index is not green; not all shards are active")); } + + return new Result(false, new Info("index is not green; not all shards are active")); } static final class Info implements ToXContentObject { @@ -144,8 +141,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (o == null) { + return false; + } + if (getClass() != o.getClass()) { + return false; + } Info info = (Info) o; return Objects.equals(getMessage(), info.getMessage()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index ef88e461c0d13..042c12b9fcb78 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -41,7 +41,7 @@ static ForceMergeAction randomInstance() { static Codec createRandomCompressionSettings() { if(randomBoolean()) { - return Codec.getDefault(); + return null; } return Codec.forName(CodecService.BEST_COMPRESSION_CODEC); } @@ -50,7 +50,7 @@ static Codec createRandomCompressionSettings() { protected ForceMergeAction mutateInstance(ForceMergeAction instance) { int maxNumSegments = instance.getMaxNumSegments(); maxNumSegments = maxNumSegments + randomIntBetween(1, 10); - return new ForceMergeAction(maxNumSegments, Codec.getDefault()); + return new ForceMergeAction(maxNumSegments, createRandomCompressionSettings()); } @Override @@ -81,12 +81,13 @@ private void assertBestCompression(ForceMergeAction instance) { StepKey nextStepKey = new StepKey(randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10)); List steps = instance.toSteps(null, phase, nextStepKey); assertNotNull(steps); - assertEquals(5, steps.size()); + assertEquals(6, steps.size()); CloseIndexStep firstStep = (CloseIndexStep) steps.get(0); UpdateSettingsStep secondStep = (UpdateSettingsStep) steps.get(1); OpenIndexStep thirdStep = (OpenIndexStep) steps.get(2); WaitForIndexColorStep fourthStep = (WaitForIndexColorStep) steps.get(3); ForceMergeStep fifthStep = (ForceMergeStep) steps.get(4); + SegmentCountStep sixthStep = (SegmentCountStep) steps.get(4); assertThat(firstStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, CloseIndexStep.NAME))); assertThat(firstStep.getNextStepKey(), equalTo(secondStep.getKey())); assertThat(secondStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, UpdateSettingsStep.NAME))); @@ -98,6 +99,8 @@ private void assertBestCompression(ForceMergeAction instance) { assertThat(fourthStep.getNextStepKey(), equalTo(fifthStep)); assertThat(fifthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, ForceMergeStep.NAME))); assertThat(fifthStep.getNextStepKey(), equalTo(nextStepKey)); + assertThat(sixthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, SegmentCountStep.NAME))); + assertThat(sixthStep.getNextStepKey(), equalTo(nextStepKey)); } public void testMissingMaxNumSegments() throws IOException { @@ -105,18 +108,22 @@ public void testMissingMaxNumSegments() throws IOException { XContentParser parser = XContentHelper.createParser(null, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, emptyObject, XContentType.JSON); Exception e = expectThrows(IllegalArgumentException.class, () -> ForceMergeAction.parse(parser)); - assertThat(e.getMessage(), equalTo("Required [max_num_segments, best_compression]")); + assertThat(e.getMessage(), equalTo("Required [max_num_segments]")); } public void testInvalidNegativeSegmentNumber() { Exception r = expectThrows(IllegalArgumentException.class, () -> new - ForceMergeAction(randomIntBetween(-10, 0), Codec.getDefault())); + ForceMergeAction(randomIntBetween(-10, 0), null)); assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer")); } + public void testInvalidCodec() { + + } + public void testToSteps() { ForceMergeAction instance = createTestInstance(); - if (CodecService.BEST_COMPRESSION_CODEC.equals(instance.getCodec().getName())) { + if (instance.getCodec() != null && CodecService.BEST_COMPRESSION_CODEC.equals(instance.getCodec().getName())) { assertBestCompression(instance); } else { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java index 1469e40e6682d..e1df27f09a44f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java @@ -493,7 +493,7 @@ private ConcurrentMap convertActionNamesToActions(Strin case DeleteAction.NAME: return new DeleteAction(); case ForceMergeAction.NAME: - return new ForceMergeAction(1, Codec.getDefault()); + return new ForceMergeAction(1, null); case ReadOnlyAction.NAME: return new ReadOnlyAction(); case RolloverAction.NAME: From 80b6144d3023a894a29b803c9c738eb0c35dbc1e Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Thu, 19 Dec 2019 22:41:19 -0800 Subject: [PATCH 015/686] Adding tests for review --- .../xpack/core/ilm/ForceMergeAction.java | 4 ++-- .../xpack/core/ilm/ForceMergeActionTests.java | 4 +++- .../xpack/ilm/TimeSeriesLifecycleActionsIT.java | 11 +++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index 6ee7c3eb2df19..c3c0288c9b315 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -61,8 +61,8 @@ public ForceMergeAction(int maxNumSegments, @Nullable Codec codec) { + "] must be a positive integer"); } this.maxNumSegments = maxNumSegments; - if (codec != null && Codec.forName(codec.getName()) == null) { - throw new IllegalArgumentException("Compression type of " + codec.getName() + "does not exist"); + if (codec != null && CodecService.BEST_COMPRESSION_CODEC.equals(codec.getName()) == false) { + throw new IllegalArgumentException("Compression type not found instead " + codec.getName() + " is used."); } this.codec = codec; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index 042c12b9fcb78..a81a6e1350da6 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -118,7 +118,9 @@ public void testInvalidNegativeSegmentNumber() { } public void testInvalidCodec() { - + Exception r = expectThrows(IllegalArgumentException.class, () -> new + ForceMergeAction(randomIntBetween(-10, 0), Codec.forName(CodecService.DEFAULT_CODEC))); + assertThat(r.getMessage(), equalTo("Best Compression type not found instead " + CodecService.DEFAULT_CODEC + " is used.")); } public void testToSteps() { diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index f1de3fe738820..4fb6c87ad012f 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.codec.CodecService; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xpack.core.ilm.AllocateAction; import org.elasticsearch.xpack.core.ilm.DeleteAction; @@ -388,7 +389,7 @@ public void testReadOnly() throws Exception { } @SuppressWarnings("unchecked") - public void testForceMergeAction() throws Exception { + public void forceMergeActionWithCodec(Codec codec) throws Exception { createIndexWithSettings(index, Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)); for (int i = 0; i < randomIntBetween(2, 10); i++) { @@ -412,7 +413,7 @@ public void testForceMergeAction() throws Exception { }; assertThat(numSegments.get(), greaterThan(1)); - createNewSingletonPolicy("warm", new ForceMergeAction(1, Codec.getDefault())); + createNewSingletonPolicy("warm", new ForceMergeAction(1, codec)); updatePolicy(index, policy); assertBusy(() -> { @@ -424,6 +425,12 @@ public void testForceMergeAction() throws Exception { expectThrows(ResponseException.class, this::indexDocument); } + public void testForceMergeAction() throws Exception { + forceMergeActionWithCodec(null); + forceMergeActionWithCodec(Codec.forName(CodecService.BEST_COMPRESSION_CODEC)); + } + + public void testShrinkAction() throws Exception { int numShards = 6; int divisor = randomFrom(2, 3, 6); From 0d8138c85aa92a4f142054583433c94cbc918c1d Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Thu, 9 Jan 2020 21:40:56 -0800 Subject: [PATCH 016/686] Review comments --- .../xpack/core/ilm/CloseIndexStep.java | 9 +++++++-- .../xpack/core/ilm/ForceMergeAction.java | 19 +++++++++++++------ .../xpack/core/ilm/OpenIndexStep.java | 11 +++++++++-- .../xpack/core/ilm/WaitForIndexColorStep.java | 7 ++++++- .../xpack/core/ilm/ForceMergeActionTests.java | 2 +- .../ilm/TimeseriesLifecycleTypeTests.java | 3 +-- .../ilm/TimeSeriesLifecycleActionsIT.java | 2 +- 7 files changed, 38 insertions(+), 15 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java index 994ddf2590b58..c5bdb620ddcca 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -31,7 +31,9 @@ public void performAction(IndexMetaData indexMetaData, ClusterState currentClust CloseIndexRequest request = new CloseIndexRequest(indexMetaData.getIndex().getName()); getClient().admin().indices() .close(request, ActionListener.wrap(closeIndexResponse -> { - assert closeIndexResponse.isAcknowledged() : "close index response is not acknowledged"; + if (closeIndexResponse.isAcknowledged() == false) { + new ErrorStep(getKey()); + } listener.onResponse(true); }, listener::onFailure)); } @@ -40,5 +42,8 @@ public void performAction(IndexMetaData indexMetaData, ClusterState currentClust } } - + @Override + public boolean isRetryable() { + return true; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index c3c0288c9b315..502ad7722d105 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -62,7 +62,7 @@ public ForceMergeAction(int maxNumSegments, @Nullable Codec codec) { } this.maxNumSegments = maxNumSegments; if (codec != null && CodecService.BEST_COMPRESSION_CODEC.equals(codec.getName()) == false) { - throw new IllegalArgumentException("Compression type not found instead " + codec.getName() + " is used."); + throw new IllegalArgumentException("unknown index codec: [" + codec.getName() + "]"); } this.codec = codec; } @@ -70,7 +70,12 @@ public ForceMergeAction(int maxNumSegments, @Nullable Codec codec) { public ForceMergeAction(StreamInput in) throws IOException { this.maxNumSegments = in.readVInt(); if (in.getVersion().onOrAfter(Version.V_8_0_0)) { - this.codec = Codec.forName(in.readOptionalString()); + String codecStr = in.readOptionalString(); + if (codecStr == null) { + this.codec = null; + } else { + this.codec = Codec.forName(codecStr); + } } else { this.codec = null; } @@ -88,9 +93,11 @@ public Codec getCodec() { public void writeTo(StreamOutput out) throws IOException { out.writeVInt(maxNumSegments); if (out.getVersion().onOrAfter(Version.V_8_0_0)) { - out.writeOptionalString(codec.getName()); - } else { - out.writeString(null); + if (codec == null) { + out.writeOptionalString(null); + } else { + out.writeOptionalString(codec.getName()); + } } } @@ -170,7 +177,7 @@ public boolean equals(Object obj) { } ForceMergeAction other = (ForceMergeAction) obj; return Objects.equals(maxNumSegments, other.maxNumSegments) - && ((codec == null && other.codec == null) || Objects.equals(codec, other.codec)); + && Objects.equals(codec, other.codec); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java index 907750e212332..86e58815712d7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java @@ -32,8 +32,10 @@ public void performAction(IndexMetaData indexMetaData, ClusterState currentClust OpenIndexRequest request = new OpenIndexRequest(indexMetaData.getIndex().getName()); getClient().admin().indices() .open(request, - ActionListener.wrap(openIndexResponse-> { - assert openIndexResponse.isAcknowledged() : "open Index response is not acknowledged"; + ActionListener.wrap(openIndexResponse -> { + if (openIndexResponse.isAcknowledged() == false) { + new ErrorStep(getKey()); + } listener.onResponse(true); }, listener::onFailure)); @@ -41,4 +43,9 @@ public void performAction(IndexMetaData indexMetaData, ClusterState currentClust listener.onResponse(true); } } + + @Override + public boolean isRetryable() { + return true; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java index bfaf6ab5bfcb1..fd02a69999c73 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java @@ -27,7 +27,7 @@ class WaitForIndexColorStep extends ClusterStateWaitStep { - static final String NAME = "wait-for-index-color-step"; + static final String NAME = "wait-for-index-color"; private final ClusterHealthStatus color; @@ -79,6 +79,11 @@ public Result isConditionMet(Index index, ClusterState clusterState) { return result; } + @Override + public boolean isRetryable() { + return true; + } + private Result waitForRed(IndexRoutingTable indexRoutingTable) { if (indexRoutingTable == null) { return new Result(true, new Info("index is red")); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index a81a6e1350da6..e581237177b17 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -40,7 +40,7 @@ static ForceMergeAction randomInstance() { } static Codec createRandomCompressionSettings() { - if(randomBoolean()) { + if (randomBoolean()) { return null; } return Codec.forName(CodecService.BEST_COMPRESSION_CODEC); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java index e1df27f09a44f..92fb7d224cfe1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ilm; -import org.apache.lucene.codecs.Codec; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; @@ -35,7 +34,7 @@ public class TimeseriesLifecycleTypeTests extends ESTestCase { private static final AllocateAction TEST_ALLOCATE_ACTION = new AllocateAction(2, Collections.singletonMap("node", "node1"),null, null); private static final DeleteAction TEST_DELETE_ACTION = new DeleteAction(); - private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1, Codec.getDefault()); + private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1, null); private static final RolloverAction TEST_ROLLOVER_ACTION = new RolloverAction(new ByteSizeValue(1), null, null); private static final ShrinkAction TEST_SHRINK_ACTION = new ShrinkAction(1); private static final ReadOnlyAction TEST_READ_ONLY_ACTION = new ReadOnlyAction(); diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 4fb6c87ad012f..2705f568b5de4 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -1015,7 +1015,7 @@ private void createFullPolicy(TimeValue hotTime) throws IOException { hotActions.put(RolloverAction.NAME, new RolloverAction(null, null, 1L)); Map warmActions = new HashMap<>(); warmActions.put(SetPriorityAction.NAME, new SetPriorityAction(50)); - warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, Codec.getDefault())); + warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, null)); warmActions.put(AllocateAction.NAME, new AllocateAction(1, singletonMap("_name", "integTest-1,integTest-2"), null, null)); warmActions.put(ShrinkAction.NAME, new ShrinkAction(1)); Map coldActions = new HashMap<>(); From e58c711dba3cf8c287a15f35a26a068844ede6d5 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Sat, 18 Jan 2020 11:09:22 -0800 Subject: [PATCH 017/686] Adding Elastic Exception --- .../java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java | 3 ++- .../java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java index c5bdb620ddcca..01aa3125706e2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.ilm; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.close.CloseIndexRequest; import org.elasticsearch.client.Client; @@ -32,7 +33,7 @@ public void performAction(IndexMetaData indexMetaData, ClusterState currentClust getClient().admin().indices() .close(request, ActionListener.wrap(closeIndexResponse -> { if (closeIndexResponse.isAcknowledged() == false) { - new ErrorStep(getKey()); + throw new ElasticsearchException("close index request failed to be acknowledged"); } listener.onResponse(true); }, listener::onFailure)); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java index 86e58815712d7..56cd50025def3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.ilm; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.open.OpenIndexRequest; import org.elasticsearch.client.Client; @@ -34,7 +35,7 @@ public void performAction(IndexMetaData indexMetaData, ClusterState currentClust .open(request, ActionListener.wrap(openIndexResponse -> { if (openIndexResponse.isAcknowledged() == false) { - new ErrorStep(getKey()); + throw new ElasticsearchException("open index request failed to be acknowledged"); } listener.onResponse(true); }, listener::onFailure)); From 8f7e0aba929d497bc54355e7cc5d146a08b8e0ed Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Sun, 19 Jan 2020 23:29:34 -0800 Subject: [PATCH 018/686] Fixing broken tests --- .../xpack/ilm/action/TransportPutLifecycleActionTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java index f2f67fa281f90..a771ef533c860 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java @@ -148,7 +148,7 @@ public void testReadStepKeys() { new Step.StepKey("phase", "set_priority", SetPriorityAction.NAME))); Map actions = new HashMap<>(); - actions.put("forcemerge", new ForceMergeAction(5)); + actions.put("forcemerge", new ForceMergeAction(5, null)); actions.put("freeze", new FreezeAction()); actions.put("allocate", new AllocateAction(1, null, null, null)); PhaseExecutionInfo pei = new PhaseExecutionInfo("policy", new Phase("wonky", TimeValue.ZERO, actions), 1, 1); From f7aba603400c52b0fada413fb3c616a88ee1604d Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 29 Nov 2019 09:58:50 +0100 Subject: [PATCH 019/686] Optimize GoogleCloudStorageHttpHandler (#49677) Removing a lot of needless buffering and array creation to reduce the significant memory usage of tests using this. The incoming stream from the `exchange` is already buffered so there is no point in adding a ton of additional buffers everywhere. --- .../gcs/GoogleCloudStorageHttpHandler.java | 87 +++++++++++-------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java index 5a359ad2c7cc0..ba2a725fed29b 100644 --- a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java +++ b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java @@ -161,7 +161,7 @@ public void handle(final HttpExchange exchange) throws IOException { // Batch https://cloud.google.com/storage/docs/json_api/v1/how-tos/batch final String uri = "/storage/v1/b/" + bucket + "/o/"; final StringBuilder batch = new StringBuilder(); - for (String line : Streams.readAllLines(new BufferedInputStream(wrappedRequest))) { + for (String line : Streams.readAllLines(wrappedRequest)) { if (line.length() == 0 || line.startsWith("--") || line.toLowerCase(Locale.ROOT).startsWith("content")) { batch.append(line).append('\n'); } else if (line.startsWith("DELETE")) { @@ -225,8 +225,13 @@ public void handle(final HttpExchange exchange) throws IOException { final int start = getContentRangeStart(range); final int end = getContentRangeEnd(range); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - long bytesRead = Streams.copy(wrappedRequest, out); + final ByteArrayOutputStream out = new ByteArrayOutputStream() { + @Override + public byte[] toByteArray() { + return buf; + } + }; + long bytesRead = Streams.copy(wrappedRequest, out, new byte[128]); int length = Math.max(end + 1, limit != null ? limit : 0); if ((int) bytesRead > length) { throw new AssertionError("Requesting more bytes than available for blob"); @@ -266,56 +271,62 @@ private String httpServerUrl(final HttpExchange exchange) { return "http://" + InetAddresses.toUriString(address.getAddress()) + ":" + address.getPort(); } + private static final Pattern NAME_PATTERN = Pattern.compile("\"name\":\"([^\"]*)\""); + public static Optional> parseMultipartRequestBody(final InputStream requestBody) throws IOException { Tuple content = null; try (BufferedInputStream in = new BufferedInputStream(new GZIPInputStream(requestBody))) { String name = null; int read; + ByteArrayOutputStream out = new ByteArrayOutputStream() { + @Override + public byte[] toByteArray() { + return buf; + } + }; while ((read = in.read()) != -1) { + out.reset(); boolean markAndContinue = false; - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - do { // search next consecutive {carriage return, new line} chars and stop - if ((char) read == '\r') { - int next = in.read(); - if (next != -1) { - if (next == '\n') { - break; - } - out.write(read); - out.write(next); - continue; + do { // search next consecutive {carriage return, new line} chars and stop + if ((char) read == '\r') { + int next = in.read(); + if (next != -1) { + if (next == '\n') { + break; } - } - out.write(read); - } while ((read = in.read()) != -1); - - final String line = new String(out.toByteArray(), UTF_8); - if (line.length() == 0 || line.equals("\r\n") || line.startsWith("--") - || line.toLowerCase(Locale.ROOT).startsWith("content")) { - markAndContinue = true; - } else if (line.startsWith("{\"bucket\":")) { - markAndContinue = true; - Matcher matcher = Pattern.compile("\"name\":\"([^\"]*)\"").matcher(line); - if (matcher.find()) { - name = matcher.group(1); + out.write(read); + out.write(next); + continue; } } - if (markAndContinue) { - in.mark(Integer.MAX_VALUE); - continue; + out.write(read); + } while ((read = in.read()) != -1); + final String bucketPrefix = "{\"bucket\":"; + final String start = new String(out.toByteArray(), 0, Math.min(out.size(), bucketPrefix.length()), UTF_8); + if (start.length() == 0 || start.equals("\r\n") || start.startsWith("--") + || start.toLowerCase(Locale.ROOT).startsWith("content")) { + markAndContinue = true; + } else if (start.startsWith(bucketPrefix)) { + markAndContinue = true; + final String line = new String(out.toByteArray(), bucketPrefix.length(), out.size() - bucketPrefix.length(), UTF_8); + Matcher matcher = NAME_PATTERN.matcher(line); + if (matcher.find()) { + name = matcher.group(1); } } + if (markAndContinue) { + in.mark(Integer.MAX_VALUE); + continue; + } if (name != null) { in.reset(); - try (ByteArrayOutputStream binary = new ByteArrayOutputStream()) { - while ((read = in.read()) != -1) { - binary.write(read); - } - binary.flush(); - byte[] tmp = binary.toByteArray(); - // removes the trailing end "\r\n--__END_OF_PART__--\r\n" which is 23 bytes long - content = Tuple.tuple(name, new BytesArray(Arrays.copyOf(tmp, tmp.length - 23))); + out.reset(); + while ((read = in.read()) != -1) { + out.write(read); } + // removes the trailing end "\r\n--__END_OF_PART__--\r\n" which is 23 bytes long + content = Tuple.tuple(name, new BytesArray(Arrays.copyOf(out.toByteArray(), out.size() - 23))); + break; } } } From 8e9bc76ac5263bfe198a3e7104462d689213dda6 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 29 Nov 2019 10:14:53 +0100 Subject: [PATCH 020/686] Make BlobStoreRepository Aware of ClusterState (#49639) This is a preliminary to #49060. It does not introduce any substantial behavior change to how the blob store repository operates. What it does is to add all the infrastructure changes around passing the cluster service to the blob store, associated test changes and a best effort approach to tracking the latest repository generation on all nodes from cluster state updates. This brings a slight improvement to the consistency by which non-master nodes (or master directly after a failover) will be able to determine the latest repository generation. It does not however do any tricky checks for the situation after a repository operation (create, delete or cleanup) that could theoretically be used to get even greater accuracy to keep this change simple. This change does not in any way alter the behavior of the blobstore repository other than adding a better "guess" for the value of the latest repo generation and is mainly intended to isolate the actual logical change to how the repository operates in #49060 --- .../repository/url/URLRepositoryPlugin.java | 6 +-- .../repositories/url/URLRepository.java | 6 +-- .../repositories/url/URLRepositoryTests.java | 5 +- .../repositories/azure/AzureRepository.java | 6 +-- .../azure/AzureRepositoryPlugin.java | 6 +-- .../azure/AzureRepositorySettingsTests.java | 5 +- .../gcs/GoogleCloudStoragePlugin.java | 6 +-- .../gcs/GoogleCloudStorageRepository.java | 6 +-- ...eCloudStorageBlobStoreRepositoryTests.java | 7 +-- .../repositories/hdfs/HdfsPlugin.java | 6 +-- .../repositories/hdfs/HdfsRepository.java | 6 +-- .../repositories/s3/S3Repository.java | 6 +-- .../repositories/s3/S3RepositoryPlugin.java | 10 ++-- .../s3/RepositoryCredentialsTests.java | 6 +-- .../s3/S3BlobStoreRepositoryTests.java | 7 +-- .../repositories/s3/S3RepositoryTests.java | 5 +- .../cluster/RepositoryCleanupInProgress.java | 9 +++- .../cluster/SnapshotDeletionsInProgress.java | 20 +++++--- .../cluster/SnapshotsInProgress.java | 11 +++- .../plugins/RepositoryPlugin.java | 6 +-- .../repositories/FilterRepository.java | 6 +++ .../repositories/RepositoriesModule.java | 6 +-- .../repositories/RepositoriesService.java | 10 +++- .../repositories/Repository.java | 10 ++++ .../repositories/RepositoryOperation.java | 35 +++++++++++++ .../blobstore/BlobStoreRepository.java | 50 +++++++++++++++++-- .../repositories/fs/FsRepository.java | 6 +-- .../snapshots/SnapshotsService.java | 10 ++-- .../repositories/RepositoriesModuleTests.java | 26 +++++----- .../RepositoriesServiceTests.java | 5 ++ .../BlobStoreRepositoryRestoreTests.java | 3 +- .../blobstore/BlobStoreRepositoryTests.java | 6 +-- .../repositories/fs/FsRepositoryTests.java | 4 +- ...etadataLoadingDuringSnapshotRestoreIT.java | 10 ++-- .../snapshots/SnapshotResiliencyTests.java | 22 +++----- .../MockEventuallyConsistentRepository.java | 6 +-- ...ckEventuallyConsistentRepositoryTests.java | 21 +++----- .../index/shard/RestoreOnlyRepository.java | 5 ++ .../blobstore/BlobStoreTestUtil.java | 50 +++++++++++++++++++ .../snapshots/mockstore/MockRepository.java | 11 ++-- .../java/org/elasticsearch/xpack/ccr/Ccr.java | 5 +- .../xpack/ccr/repository/CcrRepository.java | 5 ++ .../elasticsearch/xpack/core/XPackPlugin.java | 2 +- .../snapshots/SourceOnlySnapshotIT.java | 4 +- .../SourceOnlySnapshotShardTests.java | 5 +- .../core/LocalStateCompositeXPackPlugin.java | 14 +++--- 46 files changed, 329 insertions(+), 153 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/repositories/RepositoryOperation.java diff --git a/modules/repository-url/src/main/java/org/elasticsearch/plugin/repository/url/URLRepositoryPlugin.java b/modules/repository-url/src/main/java/org/elasticsearch/plugin/repository/url/URLRepositoryPlugin.java index 6e88f0e0deb54..4c3fa615937e7 100644 --- a/modules/repository-url/src/main/java/org/elasticsearch/plugin/repository/url/URLRepositoryPlugin.java +++ b/modules/repository-url/src/main/java/org/elasticsearch/plugin/repository/url/URLRepositoryPlugin.java @@ -19,6 +19,7 @@ package org.elasticsearch.plugin.repository.url; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; @@ -26,7 +27,6 @@ import org.elasticsearch.plugins.RepositoryPlugin; import org.elasticsearch.repositories.Repository; import org.elasticsearch.repositories.url.URLRepository; -import org.elasticsearch.threadpool.ThreadPool; import java.util.Arrays; import java.util.Collections; @@ -46,8 +46,8 @@ public List> getSettings() { @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { return Collections.singletonMap(URLRepository.TYPE, - metadata -> new URLRepository(metadata, env, namedXContentRegistry, threadPool)); + metadata -> new URLRepository(metadata, env, namedXContentRegistry, clusterService)); } } diff --git a/modules/repository-url/src/main/java/org/elasticsearch/repositories/url/URLRepository.java b/modules/repository-url/src/main/java/org/elasticsearch/repositories/url/URLRepository.java index 6e759756a7289..8fbe4752dc83c 100644 --- a/modules/repository-url/src/main/java/org/elasticsearch/repositories/url/URLRepository.java +++ b/modules/repository-url/src/main/java/org/elasticsearch/repositories/url/URLRepository.java @@ -22,6 +22,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; @@ -33,7 +34,6 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.repositories.RepositoryException; import org.elasticsearch.repositories.blobstore.BlobStoreRepository; -import org.elasticsearch.threadpool.ThreadPool; import java.net.MalformedURLException; import java.net.URISyntaxException; @@ -81,8 +81,8 @@ public class URLRepository extends BlobStoreRepository { * Constructs a read-only URL-based repository */ public URLRepository(RepositoryMetaData metadata, Environment environment, - NamedXContentRegistry namedXContentRegistry, ThreadPool threadPool) { - super(metadata, namedXContentRegistry, threadPool, BlobPath.cleanPath()); + NamedXContentRegistry namedXContentRegistry, ClusterService clusterService) { + super(metadata, namedXContentRegistry, clusterService, BlobPath.cleanPath()); if (URL_SETTING.exists(metadata.settings()) == false && REPOSITORIES_URL_SETTING.exists(environment.settings()) == false) { throw new RepositoryException(metadata.name(), "missing url"); diff --git a/modules/repository-url/src/test/java/org/elasticsearch/repositories/url/URLRepositoryTests.java b/modules/repository-url/src/test/java/org/elasticsearch/repositories/url/URLRepositoryTests.java index 96a82ee0b9d24..823571462d243 100644 --- a/modules/repository-url/src/test/java/org/elasticsearch/repositories/url/URLRepositoryTests.java +++ b/modules/repository-url/src/test/java/org/elasticsearch/repositories/url/URLRepositoryTests.java @@ -25,8 +25,8 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.repositories.RepositoryException; +import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; import java.nio.file.Path; @@ -35,13 +35,12 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; -import static org.mockito.Mockito.mock; public class URLRepositoryTests extends ESTestCase { private URLRepository createRepository(Settings baseSettings, RepositoryMetaData repositoryMetaData) { return new URLRepository(repositoryMetaData, TestEnvironment.newEnvironment(baseSettings), - new NamedXContentRegistry(Collections.emptyList()), mock(ThreadPool.class)) { + new NamedXContentRegistry(Collections.emptyList()), BlobStoreTestUtil.mockClusterService()) { @Override protected void assertSnapshotOrGenericThread() { // eliminate thread name check as we create repo manually on test/main threads diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java index b5c6ed70ad0d2..7b7a9108c1ef5 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java @@ -24,6 +24,7 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; @@ -32,7 +33,6 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.repositories.blobstore.BlobStoreRepository; -import org.elasticsearch.threadpool.ThreadPool; import java.util.Locale; import java.util.function.Function; @@ -79,8 +79,8 @@ public AzureRepository( final RepositoryMetaData metadata, final NamedXContentRegistry namedXContentRegistry, final AzureStorageService storageService, - final ThreadPool threadPool) { - super(metadata, namedXContentRegistry, threadPool, buildBasePath(metadata)); + final ClusterService clusterService) { + super(metadata, namedXContentRegistry, clusterService, buildBasePath(metadata)); this.chunkSize = Repository.CHUNK_SIZE_SETTING.get(metadata.settings()); this.storageService = storageService; diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java index 8815e738f9cdd..d98a7c3cbd717 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java @@ -19,6 +19,7 @@ package org.elasticsearch.repositories.azure; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsException; @@ -31,7 +32,6 @@ import org.elasticsearch.repositories.Repository; import org.elasticsearch.threadpool.ExecutorBuilder; import org.elasticsearch.threadpool.ScalingExecutorBuilder; -import org.elasticsearch.threadpool.ThreadPool; import java.util.Arrays; import java.util.Collections; @@ -60,9 +60,9 @@ AzureStorageService createAzureStoreService(final Settings settings) { @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { return Collections.singletonMap(AzureRepository.TYPE, - (metadata) -> new AzureRepository(metadata, namedXContentRegistry, azureStoreService, threadPool)); + (metadata) -> new AzureRepository(metadata, namedXContentRegistry, azureStoreService, clusterService)); } @Override diff --git a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureRepositorySettingsTests.java b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureRepositorySettingsTests.java index 341a1d1436deb..6f5373dab98cf 100644 --- a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureRepositorySettingsTests.java +++ b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureRepositorySettingsTests.java @@ -26,8 +26,8 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; +import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -42,8 +42,7 @@ private AzureRepository azureRepository(Settings settings) { .put(settings) .build(); final AzureRepository azureRepository = new AzureRepository(new RepositoryMetaData("foo", "azure", internalSettings), - NamedXContentRegistry.EMPTY, mock(AzureStorageService.class), - mock(ThreadPool.class)); + NamedXContentRegistry.EMPTY, mock(AzureStorageService.class), BlobStoreTestUtil.mockClusterService()); assertThat(azureRepository.getBlobStore(), is(nullValue())); return azureRepository; } diff --git a/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStoragePlugin.java b/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStoragePlugin.java index 70c4dcf3a9889..c4a065270be69 100644 --- a/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStoragePlugin.java +++ b/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStoragePlugin.java @@ -19,6 +19,7 @@ package org.elasticsearch.repositories.gcs; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -27,7 +28,6 @@ import org.elasticsearch.plugins.ReloadablePlugin; import org.elasticsearch.plugins.RepositoryPlugin; import org.elasticsearch.repositories.Repository; -import org.elasticsearch.threadpool.ThreadPool; import java.util.Arrays; import java.util.Collections; @@ -52,9 +52,9 @@ protected GoogleCloudStorageService createStorageService() { @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { return Collections.singletonMap(GoogleCloudStorageRepository.TYPE, - metadata -> new GoogleCloudStorageRepository(metadata, namedXContentRegistry, this.storageService, threadPool)); + metadata -> new GoogleCloudStorageRepository(metadata, namedXContentRegistry, this.storageService, clusterService)); } @Override diff --git a/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java b/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java index 4b17fd6bef3ea..852b147346e06 100644 --- a/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java +++ b/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java @@ -22,6 +22,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.settings.Setting; @@ -30,7 +31,6 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.repositories.RepositoryException; import org.elasticsearch.repositories.blobstore.BlobStoreRepository; -import org.elasticsearch.threadpool.ThreadPool; import java.util.function.Function; @@ -64,8 +64,8 @@ class GoogleCloudStorageRepository extends BlobStoreRepository { final RepositoryMetaData metadata, final NamedXContentRegistry namedXContentRegistry, final GoogleCloudStorageService storageService, - final ThreadPool threadPool) { - super(metadata, namedXContentRegistry, threadPool, buildBasePath(metadata)); + final ClusterService clusterService) { + super(metadata, namedXContentRegistry, clusterService, buildBasePath(metadata)); this.storageService = storageService; this.chunkSize = getSetting(CHUNK_SIZE, metadata); diff --git a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java index ee0b59eb9d365..7a2c3d780123a 100644 --- a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java +++ b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java @@ -27,6 +27,7 @@ import fixture.gcs.FakeOAuth2HttpHandler; import fixture.gcs.GoogleCloudStorageHttpHandler; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.settings.MockSecureSettings; @@ -38,7 +39,6 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.repositories.Repository; import org.elasticsearch.repositories.blobstore.ESMockAPIBasedRepositoryIntegTestCase; -import org.elasticsearch.threadpool.ThreadPool; import org.threeten.bp.Duration; import java.io.IOException; @@ -170,9 +170,10 @@ StorageOptions createStorageOptions(final GoogleCloudStorageClientSettings clien } @Override - public Map getRepositories(Environment env, NamedXContentRegistry registry, ThreadPool threadPool) { + public Map getRepositories(Environment env, NamedXContentRegistry registry, + ClusterService clusterService) { return Collections.singletonMap(GoogleCloudStorageRepository.TYPE, - metadata -> new GoogleCloudStorageRepository(metadata, registry, this.storageService, threadPool) { + metadata -> new GoogleCloudStorageRepository(metadata, registry, this.storageService, clusterService) { @Override protected GoogleCloudStorageBlobStore createBlobStore() { return new GoogleCloudStorageBlobStore("bucket", "test", storageService) { diff --git a/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsPlugin.java b/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsPlugin.java index a6dc5fe7db140..1d93aed9cc308 100644 --- a/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsPlugin.java +++ b/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsPlugin.java @@ -30,13 +30,13 @@ import org.apache.hadoop.security.KerberosInfo; import org.apache.hadoop.security.SecurityUtil; import org.elasticsearch.SpecialPermission; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.RepositoryPlugin; import org.elasticsearch.repositories.Repository; -import org.elasticsearch.threadpool.ThreadPool; public final class HdfsPlugin extends Plugin implements RepositoryPlugin { @@ -112,7 +112,7 @@ private static Void eagerInit() { @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { - return Collections.singletonMap("hdfs", (metadata) -> new HdfsRepository(metadata, env, namedXContentRegistry, threadPool)); + ClusterService clusterService) { + return Collections.singletonMap("hdfs", (metadata) -> new HdfsRepository(metadata, env, namedXContentRegistry, clusterService)); } } diff --git a/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsRepository.java b/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsRepository.java index 72430bcd36631..776c97422e5ee 100644 --- a/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsRepository.java +++ b/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsRepository.java @@ -31,6 +31,7 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.SpecialPermission; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.blobstore.BlobPath; @@ -40,7 +41,6 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; import org.elasticsearch.repositories.blobstore.BlobStoreRepository; -import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; import java.io.UncheckedIOException; @@ -67,8 +67,8 @@ public final class HdfsRepository extends BlobStoreRepository { private static final ByteSizeValue DEFAULT_BUFFER_SIZE = new ByteSizeValue(100, ByteSizeUnit.KB); public HdfsRepository(RepositoryMetaData metadata, Environment environment, - NamedXContentRegistry namedXContentRegistry, ThreadPool threadPool) { - super(metadata, namedXContentRegistry, threadPool, BlobPath.cleanPath()); + NamedXContentRegistry namedXContentRegistry, ClusterService clusterService) { + super(metadata, namedXContentRegistry, clusterService, BlobPath.cleanPath()); this.environment = environment; this.chunkSize = metadata.settings().getAsBytesSize("chunk_size", null); diff --git a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java index af895758723f5..db6968ea71059 100644 --- a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java +++ b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java @@ -22,6 +22,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; @@ -32,7 +33,6 @@ import org.elasticsearch.monitor.jvm.JvmInfo; import org.elasticsearch.repositories.RepositoryException; import org.elasticsearch.repositories.blobstore.BlobStoreRepository; -import org.elasticsearch.threadpool.ThreadPool; import java.util.function.Function; @@ -152,8 +152,8 @@ class S3Repository extends BlobStoreRepository { final RepositoryMetaData metadata, final NamedXContentRegistry namedXContentRegistry, final S3Service service, - final ThreadPool threadPool) { - super(metadata, namedXContentRegistry, threadPool, buildBasePath(metadata)); + final ClusterService clusterService) { + super(metadata, namedXContentRegistry, clusterService, buildBasePath(metadata)); this.service = service; // Parse and validate the user's S3 Storage Class setting diff --git a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java index 461eb9e9592cf..0661d40ec804f 100644 --- a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java +++ b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java @@ -22,6 +22,7 @@ import com.amazonaws.util.json.Jackson; import org.elasticsearch.SpecialPermission; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -30,7 +31,6 @@ import org.elasticsearch.plugins.ReloadablePlugin; import org.elasticsearch.plugins.RepositoryPlugin; import org.elasticsearch.repositories.Repository; -import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; import java.security.AccessController; @@ -79,14 +79,14 @@ public S3RepositoryPlugin(final Settings settings) { protected S3Repository createRepository( final RepositoryMetaData metadata, final NamedXContentRegistry registry, - final ThreadPool threadPool) { - return new S3Repository(metadata, registry, service, threadPool); + final ClusterService clusterService) { + return new S3Repository(metadata, registry, service, clusterService); } @Override public Map getRepositories(final Environment env, final NamedXContentRegistry registry, - final ThreadPool threadPool) { - return Collections.singletonMap(S3Repository.TYPE, metadata -> createRepository(metadata, registry, threadPool)); + final ClusterService clusterService) { + return Collections.singletonMap(S3Repository.TYPE, metadata -> createRepository(metadata, registry, clusterService)); } @Override diff --git a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java index f7d5ba021e25c..45a79af102df4 100644 --- a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java +++ b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java @@ -25,6 +25,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -32,7 +33,6 @@ import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.test.ESSingleNodeTestCase; -import org.elasticsearch.threadpool.ThreadPool; import java.util.Collection; import java.util.List; @@ -146,8 +146,8 @@ public ProxyS3RepositoryPlugin(Settings settings) { @Override protected S3Repository createRepository(RepositoryMetaData metadata, - NamedXContentRegistry registry, ThreadPool threadPool) { - return new S3Repository(metadata, registry, service, threadPool) { + NamedXContentRegistry registry, ClusterService clusterService) { + return new S3Repository(metadata, registry, service, clusterService) { @Override protected void assertSnapshotOrGenericThread() { // eliminate thread name check as we create repo manually on test/main threads diff --git a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java index 16dd4cdc27e0b..3c26c99f8a528 100644 --- a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java +++ b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java @@ -23,6 +23,7 @@ import com.sun.net.httpserver.HttpHandler; import fixture.s3.S3HttpHandler; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobPath; @@ -35,7 +36,6 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.repositories.blobstore.ESMockAPIBasedRepositoryIntegTestCase; import org.elasticsearch.snapshots.mockstore.BlobStoreWrapper; -import org.elasticsearch.threadpool.ThreadPool; import java.util.ArrayList; import java.util.Collection; @@ -109,8 +109,9 @@ public List> getSettings() { } @Override - protected S3Repository createRepository(RepositoryMetaData metadata, NamedXContentRegistry registry, ThreadPool threadPool) { - return new S3Repository(metadata, registry, service, threadPool) { + protected S3Repository createRepository(RepositoryMetaData metadata, NamedXContentRegistry registry, + ClusterService clusterService) { + return new S3Repository(metadata, registry, service, clusterService) { @Override public BlobStore blobStore() { diff --git a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java index 45177dbebbf48..3466fd89ebbf9 100644 --- a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java +++ b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java @@ -26,8 +26,8 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.repositories.RepositoryException; +import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; import org.hamcrest.Matchers; import java.util.Map; @@ -36,7 +36,6 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; -import static org.mockito.Mockito.mock; public class S3RepositoryTests extends ESTestCase { @@ -120,7 +119,7 @@ public void testDefaultBufferSize() { } private S3Repository createS3Repo(RepositoryMetaData metadata) { - return new S3Repository(metadata, NamedXContentRegistry.EMPTY, new DummyS3Service(), mock(ThreadPool.class)) { + return new S3Repository(metadata, NamedXContentRegistry.EMPTY, new DummyS3Service(), BlobStoreTestUtil.mockClusterService()) { @Override protected void assertSnapshotOrGenericThread() { // eliminate thread name check as we create repo manually on test/main threads diff --git a/server/src/main/java/org/elasticsearch/cluster/RepositoryCleanupInProgress.java b/server/src/main/java/org/elasticsearch/cluster/RepositoryCleanupInProgress.java index f9be2d326980d..7c43c03ae6d40 100644 --- a/server/src/main/java/org/elasticsearch/cluster/RepositoryCleanupInProgress.java +++ b/server/src/main/java/org/elasticsearch/cluster/RepositoryCleanupInProgress.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.repositories.RepositoryOperation; import java.io.IOException; import java.util.Arrays; @@ -94,7 +95,7 @@ public Version getMinimalSupportedVersion() { return Version.V_7_4_0; } - public static final class Entry implements Writeable { + public static final class Entry implements Writeable, RepositoryOperation { private final String repository; @@ -110,6 +111,12 @@ public Entry(String repository, long repositoryStateId) { this.repositoryStateId = repositoryStateId; } + @Override + public long repositoryStateId() { + return repositoryStateId; + } + + @Override public String repository() { return repository; } diff --git a/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java b/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java index 2ac12d3e93922..9c25c5578f315 100644 --- a/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java +++ b/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.repositories.RepositoryOperation; import org.elasticsearch.snapshots.Snapshot; import java.io.IOException; @@ -164,7 +165,7 @@ public String toString() { /** * A class representing a snapshot deletion request entry in the cluster state. */ - public static final class Entry implements Writeable { + public static final class Entry implements Writeable, RepositoryOperation { private final Snapshot snapshot; private final long startTime; private final long repositoryStateId; @@ -195,13 +196,6 @@ public long getStartTime() { return startTime; } - /** - * The repository state id at the time the snapshot deletion began. - */ - public long getRepositoryStateId() { - return repositoryStateId; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -227,5 +221,15 @@ public void writeTo(StreamOutput out) throws IOException { out.writeVLong(startTime); out.writeLong(repositoryStateId); } + + @Override + public String repository() { + return snapshot.getRepository(); + } + + @Override + public long repositoryStateId() { + return repositoryStateId; + } } } diff --git a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java b/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java index b40d771c64739..7ddecd8b2fdb3 100644 --- a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java +++ b/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.repositories.IndexId; +import org.elasticsearch.repositories.RepositoryOperation; import org.elasticsearch.snapshots.Snapshot; import org.elasticsearch.snapshots.SnapshotsService; @@ -81,7 +82,7 @@ public String toString() { return builder.append("]").toString(); } - public static class Entry implements ToXContent { + public static class Entry implements ToXContent, RepositoryOperation { private final State state; private final Snapshot snapshot; private final boolean includeGlobalState; @@ -153,6 +154,11 @@ public Entry(Entry entry, ImmutableOpenMap shards) this(entry, entry.state, shards, entry.failure); } + @Override + public String repository() { + return snapshot.getRepository(); + } + public Snapshot snapshot() { return this.snapshot; } @@ -189,7 +195,8 @@ public long startTime() { return startTime; } - public long getRepositoryStateId() { + @Override + public long repositoryStateId() { return repositoryStateId; } diff --git a/server/src/main/java/org/elasticsearch/plugins/RepositoryPlugin.java b/server/src/main/java/org/elasticsearch/plugins/RepositoryPlugin.java index ede5c5e3611f9..1ac61b27fd1ae 100644 --- a/server/src/main/java/org/elasticsearch/plugins/RepositoryPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/RepositoryPlugin.java @@ -22,10 +22,10 @@ import java.util.Collections; import java.util.Map; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; import org.elasticsearch.repositories.Repository; -import org.elasticsearch.threadpool.ThreadPool; /** * An extension point for {@link Plugin} implementations to add custom snapshot repositories. @@ -41,7 +41,7 @@ public interface RepositoryPlugin { * the value is a factory to construct the {@link Repository} interface. */ default Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { return Collections.emptyMap(); } @@ -55,7 +55,7 @@ default Map getRepositories(Environment env, NamedXC * the value is a factory to construct the {@link Repository} interface. */ default Map getInternalRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { return Collections.emptyMap(); } } diff --git a/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java b/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java index 7f4d5ec5c1fda..76645834fb112 100644 --- a/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java @@ -20,6 +20,7 @@ import org.apache.lucene.index.IndexCommit; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.metadata.RepositoryMetaData; @@ -133,6 +134,11 @@ public IndexShardSnapshotStatus getShardSnapshotStatus(SnapshotId snapshotId, In return in.getShardSnapshotStatus(snapshotId, indexId, shardId); } + @Override + public void updateState(ClusterState state) { + in.updateState(state); + } + @Override public Lifecycle.State lifecycleState() { return in.lifecycleState(); diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesModule.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesModule.java index 783e3ff78fc4d..f87aab460fcbc 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesModule.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesModule.java @@ -43,10 +43,10 @@ public final class RepositoriesModule { public RepositoriesModule(Environment env, List repoPlugins, TransportService transportService, ClusterService clusterService, ThreadPool threadPool, NamedXContentRegistry namedXContentRegistry) { Map factories = new HashMap<>(); - factories.put(FsRepository.TYPE, metadata -> new FsRepository(metadata, env, namedXContentRegistry, threadPool)); + factories.put(FsRepository.TYPE, metadata -> new FsRepository(metadata, env, namedXContentRegistry, clusterService)); for (RepositoryPlugin repoPlugin : repoPlugins) { - Map newRepoTypes = repoPlugin.getRepositories(env, namedXContentRegistry, threadPool); + Map newRepoTypes = repoPlugin.getRepositories(env, namedXContentRegistry, clusterService); for (Map.Entry entry : newRepoTypes.entrySet()) { if (factories.put(entry.getKey(), entry.getValue()) != null) { throw new IllegalArgumentException("Repository type [" + entry.getKey() + "] is already registered"); @@ -56,7 +56,7 @@ public RepositoriesModule(Environment env, List repoPlugins, T Map internalFactories = new HashMap<>(); for (RepositoryPlugin repoPlugin : repoPlugins) { - Map newRepoTypes = repoPlugin.getInternalRepositories(env, namedXContentRegistry, threadPool); + Map newRepoTypes = repoPlugin.getInternalRepositories(env, namedXContentRegistry, clusterService); for (Map.Entry entry : newRepoTypes.entrySet()) { if (internalFactories.put(entry.getKey(), entry.getValue()) != null) { throw new IllegalArgumentException("Internal repository type [" + entry.getKey() + "] is already registered"); diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 20f083dcff0fe..03b283c4aafe5 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -287,8 +287,9 @@ protected void doRun() { @Override public void applyClusterState(ClusterChangedEvent event) { try { + final ClusterState state = event.state(); RepositoriesMetaData oldMetaData = event.previousState().getMetaData().custom(RepositoriesMetaData.TYPE); - RepositoriesMetaData newMetaData = event.state().getMetaData().custom(RepositoriesMetaData.TYPE); + RepositoriesMetaData newMetaData = state.getMetaData().custom(RepositoriesMetaData.TYPE); // Check if repositories got changed if ((oldMetaData == null && newMetaData == null) || (oldMetaData != null && oldMetaData.equals(newMetaData))) { @@ -344,6 +345,9 @@ public void applyClusterState(ClusterChangedEvent event) { } } } + for (Repository repo : builder.values()) { + repo.updateState(state); + } repositories = Collections.unmodifiableMap(builder); } catch (Exception ex) { logger.warn("failure updating cluster state ", ex); @@ -411,11 +415,13 @@ private Repository createRepository(RepositoryMetaData repositoryMetaData, Map Math.max(known, finalBestGen)); + } + + private long bestGeneration(Collection operations) { + final String repoName = metadata.name(); + assert operations.size() <= 1 : "Assumed one or no operations but received " + operations; + return operations.stream().filter(e -> e.repository().equals(repoName)).mapToLong(RepositoryOperation::repositoryStateId) + .max().orElse(RepositoryData.EMPTY_REPO_GEN); + } + public ThreadPool threadPool() { return threadPool; } diff --git a/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java b/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java index 61558e4f42efa..efe095eb9b6c2 100644 --- a/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java @@ -22,6 +22,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; import org.elasticsearch.common.blobstore.fs.FsBlobStore; @@ -32,7 +33,6 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.repositories.RepositoryException; import org.elasticsearch.repositories.blobstore.BlobStoreRepository; -import org.elasticsearch.threadpool.ThreadPool; import java.nio.file.Path; import java.util.function.Function; @@ -70,8 +70,8 @@ public class FsRepository extends BlobStoreRepository { * Constructs a shared file system repository. */ public FsRepository(RepositoryMetaData metadata, Environment environment, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { - super(metadata, namedXContentRegistry, threadPool, BlobPath.cleanPath()); + ClusterService clusterService) { + super(metadata, namedXContentRegistry, clusterService, BlobPath.cleanPath()); this.environment = environment; String location = REPOSITORIES_LOCATION_SETTING.get(metadata.settings()); if (location.isEmpty()) { diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index 0ac0204760ea3..c38462e240747 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -573,7 +573,7 @@ private void cleanupAfterError(Exception exception) { ExceptionsHelper.stackTrace(exception), 0, Collections.emptyList(), - snapshot.getRepositoryStateId(), + snapshot.repositoryStateId(), snapshot.includeGlobalState(), metaDataForSnapshot(snapshot, clusterService.state().metaData()), snapshot.userMetadata(), @@ -782,7 +782,7 @@ private void finalizeSnapshotDeletionFromPreviousMaster(ClusterState state) { if (deletionsInProgress != null && deletionsInProgress.hasDeletionsInProgress()) { assert deletionsInProgress.getEntries().size() == 1 : "only one in-progress deletion allowed per cluster"; SnapshotDeletionsInProgress.Entry entry = deletionsInProgress.getEntries().get(0); - deleteSnapshotFromRepository(entry.getSnapshot(), null, entry.getRepositoryStateId(), + deleteSnapshotFromRepository(entry.getSnapshot(), null, entry.repositoryStateId(), state.nodes().getMinNodeVersion()); } } @@ -852,7 +852,7 @@ public void onResponse(Void aVoid) { public void onFailure(Exception e) { logger.warn("failed to clean up abandoned snapshot {} in INIT state", snapshot.snapshot()); } - }, updatedSnapshot.getRepositoryStateId(), false); + }, updatedSnapshot.repositoryStateId(), false); } assert updatedSnapshot.shards().size() == snapshot.shards().size() : "Shard count changed during snapshot status update from [" + snapshot + "] to [" + updatedSnapshot + "]"; @@ -1037,7 +1037,7 @@ protected void doRun() { failure, entry.shards().size(), unmodifiableList(shardFailures), - entry.getRepositoryStateId(), + entry.repositoryStateId(), entry.includeGlobalState(), metaDataForSnapshot(entry, metaData), entry.userMetadata(), @@ -1163,7 +1163,7 @@ public void deleteSnapshot(final String repositoryName, final String snapshotNam if (matchedInProgress.isPresent()) { matchedEntry = matchedInProgress.map(s -> s.snapshot().getSnapshotId()); // Derive repository generation if a snapshot is in progress because it will increment the generation when it finishes - repoGenId = matchedInProgress.get().getRepositoryStateId() + 1L; + repoGenId = matchedInProgress.get().repositoryStateId() + 1L; } } if (matchedEntry.isPresent() == false) { diff --git a/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java b/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java index cd31ce121b245..1fad6f62773af 100644 --- a/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java @@ -44,6 +44,7 @@ public class RepositoriesModuleTests extends ESTestCase { private RepositoryPlugin plugin2; private Repository.Factory factory; private ThreadPool threadPool; + private ClusterService clusterService; @Override public void setUp() throws Exception { @@ -51,6 +52,7 @@ public void setUp() throws Exception { environment = mock(Environment.class); contentRegistry = mock(NamedXContentRegistry.class); threadPool = mock(ThreadPool.class); + clusterService = mock(ClusterService.class); plugin1 = mock(RepositoryPlugin.class); plugin2 = mock(RepositoryPlugin.class); factory = mock(Repository.Factory.class); @@ -60,8 +62,8 @@ public void setUp() throws Exception { } public void testCanRegisterTwoRepositoriesWithDifferentTypes() { - when(plugin1.getRepositories(environment, contentRegistry, threadPool)).thenReturn(Collections.singletonMap("type1", factory)); - when(plugin2.getRepositories(environment, contentRegistry, threadPool)).thenReturn(Collections.singletonMap("type2", factory)); + when(plugin1.getRepositories(environment, contentRegistry, clusterService)).thenReturn(Collections.singletonMap("type1", factory)); + when(plugin2.getRepositories(environment, contentRegistry, clusterService)).thenReturn(Collections.singletonMap("type2", factory)); // Would throw new RepositoriesModule( @@ -69,37 +71,37 @@ public void testCanRegisterTwoRepositoriesWithDifferentTypes() { } public void testCannotRegisterTwoRepositoriesWithSameTypes() { - when(plugin1.getRepositories(environment, contentRegistry, threadPool)).thenReturn(Collections.singletonMap("type1", factory)); - when(plugin2.getRepositories(environment, contentRegistry, threadPool)).thenReturn(Collections.singletonMap("type1", factory)); + when(plugin1.getRepositories(environment, contentRegistry, clusterService)).thenReturn(Collections.singletonMap("type1", factory)); + when(plugin2.getRepositories(environment, contentRegistry, clusterService)).thenReturn(Collections.singletonMap("type1", factory)); IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, - () -> new RepositoriesModule(environment, repoPlugins, mock(TransportService.class), mock(ClusterService.class), + () -> new RepositoriesModule(environment, repoPlugins, mock(TransportService.class), clusterService, threadPool, contentRegistry)); assertEquals("Repository type [type1] is already registered", ex.getMessage()); } public void testCannotRegisterTwoInternalRepositoriesWithSameTypes() { - when(plugin1.getInternalRepositories(environment, contentRegistry, threadPool)) + when(plugin1.getInternalRepositories(environment, contentRegistry, clusterService)) .thenReturn(Collections.singletonMap("type1", factory)); - when(plugin2.getInternalRepositories(environment, contentRegistry, threadPool)) + when(plugin2.getInternalRepositories(environment, contentRegistry, clusterService)) .thenReturn(Collections.singletonMap("type1", factory)); IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, - () -> new RepositoriesModule(environment, repoPlugins, mock(TransportService.class), mock(ClusterService.class), + () -> new RepositoriesModule(environment, repoPlugins, mock(TransportService.class), clusterService, threadPool, contentRegistry)); assertEquals("Internal repository type [type1] is already registered", ex.getMessage()); } public void testCannotRegisterNormalAndInternalRepositoriesWithSameTypes() { - when(plugin1.getRepositories(environment, contentRegistry, threadPool)).thenReturn(Collections.singletonMap("type1", factory)); - when(plugin2.getInternalRepositories(environment, contentRegistry, threadPool)) + when(plugin1.getRepositories(environment, contentRegistry, clusterService)).thenReturn(Collections.singletonMap("type1", factory)); + when(plugin2.getInternalRepositories(environment, contentRegistry, clusterService)) .thenReturn(Collections.singletonMap("type1", factory)); IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, - () -> new RepositoriesModule(environment, repoPlugins, mock(TransportService.class), mock(ClusterService.class), - threadPool, contentRegistry)); + () -> new RepositoriesModule(environment, repoPlugins, mock(TransportService.class), clusterService, threadPool, + contentRegistry)); assertEquals("Internal repository type [type1] is already registered as a non-internal repository", ex.getMessage()); } diff --git a/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java b/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java index 0893a2568df17..2daa4afe3e149 100644 --- a/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java @@ -22,6 +22,7 @@ import org.apache.lucene.index.IndexCommit; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; +import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.metadata.RepositoryMetaData; @@ -214,6 +215,10 @@ public IndexShardSnapshotStatus getShardSnapshotStatus(SnapshotId snapshotId, In return null; } + @Override + public void updateState(final ClusterState state) { + } + @Override public Lifecycle.State lifecycleState() { return null; diff --git a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java index 59a977efe5f5d..b5d99db0a880f 100644 --- a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java @@ -193,7 +193,8 @@ public void testSnapshotWithConflictingName() throws IOException { private Repository createRepository() { Settings settings = Settings.builder().put("location", randomAlphaOfLength(10)).build(); RepositoryMetaData repositoryMetaData = new RepositoryMetaData(randomAlphaOfLength(10), FsRepository.TYPE, settings); - final FsRepository repository = new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), threadPool) { + final FsRepository repository = new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), + BlobStoreTestUtil.mockClusterService()) { @Override protected void assertSnapshotOrGenericThread() { // eliminate thread name check as we create repo manually diff --git a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java index 425ccdfe8ccc2..6d6248c446df1 100644 --- a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; @@ -41,7 +42,6 @@ import org.elasticsearch.snapshots.SnapshotState; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.ESSingleNodeTestCase; -import org.elasticsearch.threadpool.ThreadPool; import java.nio.file.Path; import java.util.Arrays; @@ -71,9 +71,9 @@ public static class FsLikeRepoPlugin extends Plugin implements RepositoryPlugin @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { return Collections.singletonMap(REPO_TYPE, - (metadata) -> new FsRepository(metadata, env, namedXContentRegistry, threadPool) { + (metadata) -> new FsRepository(metadata, env, namedXContentRegistry, clusterService) { @Override protected void assertSnapshotOrGenericThread() { // eliminate thread name check as we access blobStore on test/main threads diff --git a/server/src/test/java/org/elasticsearch/repositories/fs/FsRepositoryTests.java b/server/src/test/java/org/elasticsearch/repositories/fs/FsRepositoryTests.java index 5d5dc1d23e639..2b687fbcac228 100644 --- a/server/src/test/java/org/elasticsearch/repositories/fs/FsRepositoryTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/fs/FsRepositoryTests.java @@ -54,6 +54,7 @@ import org.elasticsearch.index.store.Store; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.repositories.IndexId; +import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; import org.elasticsearch.snapshots.Snapshot; import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.test.DummyShardLock; @@ -90,7 +91,8 @@ public void testSnapshotAndRestore() throws IOException, InterruptedException { int numDocs = indexDocs(directory); RepositoryMetaData metaData = new RepositoryMetaData("test", "fs", settings); - FsRepository repository = new FsRepository(metaData, new Environment(settings, null), NamedXContentRegistry.EMPTY, threadPool); + FsRepository repository = new FsRepository(metaData, new Environment(settings, null), NamedXContentRegistry.EMPTY, + BlobStoreTestUtil.mockClusterService()); repository.start(); final Settings indexSettings = Settings.builder().put(IndexMetaData.SETTING_INDEX_UUID, "myindexUUID").build(); IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("myindex", indexSettings); diff --git a/server/src/test/java/org/elasticsearch/snapshots/MetadataLoadingDuringSnapshotRestoreIT.java b/server/src/test/java/org/elasticsearch/snapshots/MetadataLoadingDuringSnapshotRestoreIT.java index 364d2efe311b9..3d65782d4824d 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/MetadataLoadingDuringSnapshotRestoreIT.java +++ b/server/src/test/java/org/elasticsearch/snapshots/MetadataLoadingDuringSnapshotRestoreIT.java @@ -26,6 +26,7 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; @@ -35,7 +36,6 @@ import org.elasticsearch.repositories.Repository; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.snapshots.mockstore.MockRepository; -import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; import java.util.Collection; @@ -185,8 +185,8 @@ public static class CountingMockRepository extends MockRepository { public CountingMockRepository(final RepositoryMetaData metadata, final Environment environment, - final NamedXContentRegistry namedXContentRegistry, ThreadPool threadPool) { - super(metadata, environment, namedXContentRegistry, threadPool); + final NamedXContentRegistry namedXContentRegistry, ClusterService clusterService) { + super(metadata, environment, namedXContentRegistry, clusterService); } @Override @@ -206,9 +206,9 @@ public IndexMetaData getSnapshotIndexMetaData(SnapshotId snapshotId, IndexId ind public static class CountingMockRepositoryPlugin extends MockRepository.Plugin { @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { return Collections.singletonMap("coutingmock", - metadata -> new CountingMockRepository(metadata, env, namedXContentRegistry, threadPool)); + metadata -> new CountingMockRepository(metadata, env, namedXContentRegistry, clusterService)); } } } diff --git a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java index c4d27ff13fca8..124bf290677bb 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java @@ -1226,23 +1226,15 @@ searchTransportService, new SearchPhaseController(searchService::createReduceCon private Repository.Factory getRepoFactory(Environment environment) { // Run half the tests with the eventually consistent repository if (blobStoreContext == null) { - return metaData -> { - final Repository repository = new FsRepository(metaData, environment, xContentRegistry(), threadPool) { - @Override - protected void assertSnapshotOrGenericThread() { - // eliminate thread name check as we create repo in the test thread - } - }; - repository.start(); - return repository; + return metaData -> new FsRepository(metaData, environment, xContentRegistry(), clusterService) { + @Override + protected void assertSnapshotOrGenericThread() { + // eliminate thread name check as we create repo in the test thread + } }; } else { - return metaData -> { - final Repository repository = new MockEventuallyConsistentRepository( - metaData, xContentRegistry(), deterministicTaskQueue.getThreadPool(), blobStoreContext, random()); - repository.start(); - return repository; - }; + return metaData -> + new MockEventuallyConsistentRepository(metaData, xContentRegistry(), clusterService, blobStoreContext, random()); } } public void restart() { diff --git a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java index 9727dc5f28283..0931ceb494827 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java +++ b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java @@ -21,6 +21,7 @@ import org.apache.lucene.codecs.CodecUtil; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobMetaData; @@ -37,7 +38,6 @@ import org.elasticsearch.repositories.blobstore.BlobStoreRepository; import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -74,10 +74,10 @@ public class MockEventuallyConsistentRepository extends BlobStoreRepository { public MockEventuallyConsistentRepository( final RepositoryMetaData metadata, final NamedXContentRegistry namedXContentRegistry, - final ThreadPool threadPool, + final ClusterService clusterService, final Context context, final Random random) { - super(metadata, namedXContentRegistry, threadPool, BlobPath.cleanPath()); + super(metadata, namedXContentRegistry, clusterService, BlobPath.cleanPath()); this.context = context; this.namedXContentRegistry = namedXContentRegistry; this.random = random; diff --git a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java index 5fdfbe9a93a4d..f1cf314e3158a 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java @@ -18,7 +18,6 @@ */ package org.elasticsearch.snapshots.mockstore; -import org.apache.lucene.util.SameThreadExecutorService; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.metadata.RepositoryMetaData; @@ -27,10 +26,10 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.repositories.ShardGenerations; import org.elasticsearch.repositories.blobstore.BlobStoreRepository; +import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -41,8 +40,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.startsWith; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class MockEventuallyConsistentRepositoryTests extends ESTestCase { @@ -50,7 +47,7 @@ public void testReadAfterWriteConsistently() throws IOException { MockEventuallyConsistentRepository.Context blobStoreContext = new MockEventuallyConsistentRepository.Context(); try (BlobStoreRepository repository = new MockEventuallyConsistentRepository( new RepositoryMetaData("testRepo", "mockEventuallyConsistent", Settings.EMPTY), - xContentRegistry(), mock(ThreadPool.class), blobStoreContext, random())) { + xContentRegistry(), BlobStoreTestUtil.mockClusterService(), blobStoreContext, random())) { repository.start(); final BlobContainer blobContainer = repository.blobStore().blobContainer(repository.basePath()); final String blobName = randomAlphaOfLength(10); @@ -70,7 +67,7 @@ public void testReadAfterWriteAfterReadThrows() throws IOException { MockEventuallyConsistentRepository.Context blobStoreContext = new MockEventuallyConsistentRepository.Context(); try (BlobStoreRepository repository = new MockEventuallyConsistentRepository( new RepositoryMetaData("testRepo", "mockEventuallyConsistent", Settings.EMPTY), - xContentRegistry(), mock(ThreadPool.class), blobStoreContext, random())) { + xContentRegistry(), BlobStoreTestUtil.mockClusterService(), blobStoreContext, random())) { repository.start(); final BlobContainer blobContainer = repository.blobStore().blobContainer(repository.basePath()); final String blobName = randomAlphaOfLength(10); @@ -86,7 +83,7 @@ public void testReadAfterDeleteAfterWriteThrows() throws IOException { MockEventuallyConsistentRepository.Context blobStoreContext = new MockEventuallyConsistentRepository.Context(); try (BlobStoreRepository repository = new MockEventuallyConsistentRepository( new RepositoryMetaData("testRepo", "mockEventuallyConsistent", Settings.EMPTY), - xContentRegistry(), mock(ThreadPool.class), blobStoreContext, random())) { + xContentRegistry(), BlobStoreTestUtil.mockClusterService(), blobStoreContext, random())) { repository.start(); final BlobContainer blobContainer = repository.blobStore().blobContainer(repository.basePath()); final String blobName = randomAlphaOfLength(10); @@ -104,7 +101,7 @@ public void testOverwriteRandomBlobFails() throws IOException { MockEventuallyConsistentRepository.Context blobStoreContext = new MockEventuallyConsistentRepository.Context(); try (BlobStoreRepository repository = new MockEventuallyConsistentRepository( new RepositoryMetaData("testRepo", "mockEventuallyConsistent", Settings.EMPTY), - xContentRegistry(), mock(ThreadPool.class), blobStoreContext, random())) { + xContentRegistry(), BlobStoreTestUtil.mockClusterService(), blobStoreContext, random())) { repository.start(); final BlobContainer container = repository.blobStore().blobContainer(repository.basePath()); final String blobName = randomAlphaOfLength(10); @@ -121,7 +118,7 @@ public void testOverwriteShardSnapBlobFails() throws IOException { MockEventuallyConsistentRepository.Context blobStoreContext = new MockEventuallyConsistentRepository.Context(); try (BlobStoreRepository repository = new MockEventuallyConsistentRepository( new RepositoryMetaData("testRepo", "mockEventuallyConsistent", Settings.EMPTY), - xContentRegistry(), mock(ThreadPool.class), blobStoreContext, random())) { + xContentRegistry(), BlobStoreTestUtil.mockClusterService(), blobStoreContext, random())) { repository.start(); final BlobContainer container = repository.blobStore().blobContainer(repository.basePath().add("indices").add("someindex").add("0")); @@ -137,13 +134,9 @@ public void testOverwriteShardSnapBlobFails() throws IOException { public void testOverwriteSnapshotInfoBlob() { MockEventuallyConsistentRepository.Context blobStoreContext = new MockEventuallyConsistentRepository.Context(); - final ThreadPool threadPool = mock(ThreadPool.class); - when(threadPool.executor(ThreadPool.Names.SNAPSHOT)).thenReturn(new SameThreadExecutorService()); - when(threadPool.info(ThreadPool.Names.SNAPSHOT)).thenReturn( - new ThreadPool.Info(ThreadPool.Names.SNAPSHOT, ThreadPool.ThreadPoolType.FIXED, randomIntBetween(1, 10))); try (BlobStoreRepository repository = new MockEventuallyConsistentRepository( new RepositoryMetaData("testRepo", "mockEventuallyConsistent", Settings.EMPTY), - xContentRegistry(), threadPool, blobStoreContext, random())) { + xContentRegistry(), BlobStoreTestUtil.mockClusterService(), blobStoreContext, random())) { repository.start(); // We create a snap- blob for snapshot "foo" in the first generation diff --git a/test/framework/src/main/java/org/elasticsearch/index/shard/RestoreOnlyRepository.java b/test/framework/src/main/java/org/elasticsearch/index/shard/RestoreOnlyRepository.java index 5744504845aa6..f0118d3c0b699 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/shard/RestoreOnlyRepository.java +++ b/test/framework/src/main/java/org/elasticsearch/index/shard/RestoreOnlyRepository.java @@ -20,6 +20,7 @@ import org.apache.lucene.index.IndexCommit; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.metadata.RepositoryMetaData; @@ -142,4 +143,8 @@ public IndexShardSnapshotStatus getShardSnapshotStatus(SnapshotId snapshotId, In @Override public void verify(String verificationToken, DiscoveryNode localNode) { } + + @Override + public void updateState(final ClusterState state) { + } } diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java index 12b926f93a059..66c49db542dab 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java @@ -18,9 +18,16 @@ */ package org.elasticsearch.repositories.blobstore; +import org.apache.lucene.util.SameThreadExecutorService; import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.ClusterChangedEvent; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateApplier; +import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.service.ClusterApplierService; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobMetaData; @@ -52,9 +59,11 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import static org.elasticsearch.test.ESTestCase.randomIntBetween; @@ -66,6 +75,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public final class BlobStoreTestUtil { @@ -275,4 +289,40 @@ public static void assertBlobsByPrefix(BlobStoreRepository repository, BlobPath } } } + + /** + * Creates a mocked {@link ClusterService} for use in {@link BlobStoreRepository} related tests that mocks out all the necessary + * functionality to make {@link BlobStoreRepository} work. + * + * @return Mock ClusterService + */ + public static ClusterService mockClusterService() { + final ThreadPool threadPool = mock(ThreadPool.class); + when(threadPool.executor(ThreadPool.Names.SNAPSHOT)).thenReturn(new SameThreadExecutorService()); + when(threadPool.generic()).thenReturn(new SameThreadExecutorService()); + when(threadPool.info(ThreadPool.Names.SNAPSHOT)).thenReturn( + new ThreadPool.Info(ThreadPool.Names.SNAPSHOT, ThreadPool.ThreadPoolType.FIXED, randomIntBetween(1, 10))); + final ClusterService clusterService = mock(ClusterService.class); + final ClusterApplierService clusterApplierService = mock(ClusterApplierService.class); + when(clusterService.getClusterApplierService()).thenReturn(clusterApplierService); + final AtomicReference currentState = new AtomicReference<>(ClusterState.EMPTY_STATE); + when(clusterService.state()).then(invocationOnMock -> currentState.get()); + final List appliers = new CopyOnWriteArrayList<>(); + doAnswer(invocation -> { + final ClusterStateUpdateTask task = ((ClusterStateUpdateTask) invocation.getArguments()[1]); + final ClusterState current = currentState.get(); + final ClusterState next = task.execute(current); + currentState.set(next); + appliers.forEach(applier -> applier.applyClusterState( + new ClusterChangedEvent((String) invocation.getArguments()[0], next, current))); + task.clusterStateProcessed((String) invocation.getArguments()[0], current, next); + return null; + }).when(clusterService).submitStateUpdateTask(anyString(), any(ClusterStateUpdateTask.class)); + doAnswer(invocation -> { + appliers.add((ClusterStateApplier) invocation.getArguments()[0]); + return null; + }).when(clusterService).addStateApplier(any(ClusterStateApplier.class)); + when(clusterApplierService.threadPool()).thenReturn(threadPool); + return clusterService; + } } diff --git a/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java b/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java index 9a157e11c7227..218c6f4eecac7 100644 --- a/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java +++ b/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java @@ -25,6 +25,7 @@ import org.apache.lucene.index.CorruptIndexException; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobMetaData; import org.elasticsearch.common.blobstore.BlobPath; @@ -40,7 +41,6 @@ import org.elasticsearch.plugins.RepositoryPlugin; import org.elasticsearch.repositories.Repository; import org.elasticsearch.repositories.fs.FsRepository; -import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; import java.io.InputStream; @@ -71,8 +71,9 @@ public static class Plugin extends org.elasticsearch.plugins.Plugin implements R @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { - return Collections.singletonMap("mock", (metadata) -> new MockRepository(metadata, env, namedXContentRegistry, threadPool)); + ClusterService clusterService) { + return Collections.singletonMap("mock", (metadata) -> + new MockRepository(metadata, env, namedXContentRegistry, clusterService)); } @Override @@ -113,8 +114,8 @@ public long getFailureCount() { private volatile boolean blocked = false; public MockRepository(RepositoryMetaData metadata, Environment environment, - NamedXContentRegistry namedXContentRegistry, ThreadPool threadPool) { - super(overrideSettings(metadata, environment), environment, namedXContentRegistry, threadPool); + NamedXContentRegistry namedXContentRegistry, ClusterService clusterService) { + super(overrideSettings(metadata, environment), environment, namedXContentRegistry, clusterService); randomControlIOExceptionRate = metadata.settings().getAsDouble("random_control_io_exception_rate", 0.0); randomDataFileIOExceptionRate = metadata.settings().getAsDouble("random_data_file_io_exception_rate", 0.0); useLuceneCorruptionException = metadata.settings().getAsBoolean("use_lucene_corruption", false); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java index 9bf15e2ab030c..0fd087a47c44e 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java @@ -339,9 +339,10 @@ public List> getExecutorBuilders(Settings settings) { @Override public Map getInternalRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { Repository.Factory repositoryFactory = - (metadata) -> new CcrRepository(metadata, client, ccrLicenseChecker, settings, ccrSettings.get(), threadPool); + (metadata) -> new CcrRepository(metadata, client, ccrLicenseChecker, settings, ccrSettings.get(), + clusterService.getClusterApplierService().threadPool()); return Collections.singletonMap(CcrRepository.TYPE, repositoryFactory); } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java index 820a965e3b134..20c098ee7f82a 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java @@ -22,6 +22,7 @@ import org.elasticsearch.action.support.ThreadedActionListener; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MappingMetaData; import org.elasticsearch.cluster.metadata.MetaData; @@ -420,6 +421,10 @@ public IndexShardSnapshotStatus getShardSnapshotStatus(SnapshotId snapshotId, In throw new UnsupportedOperationException("Unsupported for repository of type: " + TYPE); } + @Override + public void updateState(ClusterState state) { + } + private void updateMappings(Client leaderClient, Index leaderIndex, long leaderMappingVersion, Client followerClient, Index followerIndex) { final PlainActionFuture indexMetadataFuture = new PlainActionFuture<>(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java index 1b20ceae9233e..02fd885b3e350 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java @@ -312,7 +312,7 @@ public static Path resolveConfigFile(Environment env, String name) { @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { return Collections.singletonMap("source", SourceOnlySnapshotRepository.newRepositoryFactory()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotIT.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotIT.java index d6e94fc8ce112..899c300493ab4 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotIT.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotIT.java @@ -16,6 +16,7 @@ import org.elasticsearch.client.Client; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.metadata.MappingMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -35,7 +36,6 @@ import org.elasticsearch.search.slice.SliceBuilder; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.threadpool.ThreadPool; import org.hamcrest.Matchers; import java.io.IOException; @@ -70,7 +70,7 @@ protected boolean addMockInternalEngine() { public static final class MyPlugin extends Plugin implements RepositoryPlugin, EnginePlugin { @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { return Collections.singletonMap("source", SourceOnlySnapshotRepository.newRepositoryFactory()); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java index f5f8287f2c5c1..03ca8d5cfff28 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java @@ -61,6 +61,7 @@ import org.elasticsearch.repositories.IndexId; import org.elasticsearch.repositories.Repository; import org.elasticsearch.repositories.ShardGenerations; +import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; import org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.threadpool.ThreadPool; @@ -348,10 +349,10 @@ private Environment createEnvironment() { } /** Create a {@link Repository} with a random name **/ - private Repository createRepository() throws IOException { + private Repository createRepository() { Settings settings = Settings.builder().put("location", randomAlphaOfLength(10)).build(); RepositoryMetaData repositoryMetaData = new RepositoryMetaData(randomAlphaOfLength(10), FsRepository.TYPE, settings); - return new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), threadPool); + return new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), BlobStoreTestUtil.mockClusterService()); } private static void runAsSnapshot(ThreadPool pool, Runnable runnable) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java index 6952342040c5d..accea03f9b68a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java @@ -406,19 +406,21 @@ public List> getPersistentTasksExecutor(ClusterServic @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { - HashMap repositories = new HashMap<>(super.getRepositories(env, namedXContentRegistry, threadPool)); - filterPlugins(RepositoryPlugin.class).forEach(r -> repositories.putAll(r.getRepositories(env, namedXContentRegistry, threadPool))); + ClusterService clusterService) { + HashMap repositories = + new HashMap<>(super.getRepositories(env, namedXContentRegistry, clusterService)); + filterPlugins(RepositoryPlugin.class).forEach( + r -> repositories.putAll(r.getRepositories(env, namedXContentRegistry, clusterService))); return repositories; } @Override public Map getInternalRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, - ThreadPool threadPool) { + ClusterService clusterService) { HashMap internalRepositories = - new HashMap<>(super.getInternalRepositories(env, namedXContentRegistry, threadPool)); + new HashMap<>(super.getInternalRepositories(env, namedXContentRegistry, clusterService)); filterPlugins(RepositoryPlugin.class).forEach(r -> - internalRepositories.putAll(r.getInternalRepositories(env, namedXContentRegistry, threadPool))); + internalRepositories.putAll(r.getInternalRepositories(env, namedXContentRegistry, clusterService))); return internalRepositories; } From 2a4860dc50f22fdb4261cfb63c361f176e908fa3 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Fri, 29 Nov 2019 11:01:24 +0100 Subject: [PATCH 021/686] Remove obsolete resolving logic from TRA (#49685) This stems from a time where index requests were directly forwarded to TransportReplicationAction. Nowadays they are wrapped in a BulkShardRequest, and this logic is obsolete. In contrast to prior PR (#49647), this PR also fixes (see b3697cc) a situation where the previous index expression logic had an interesting side effect. For bulk requests (which had resolveIndex = false), the reroute phase was waiting for the index to appear in case where it was not present, and for all other replication requests (resolveIndex = true) it would right away throw an IndexNotFoundException while resolving the name and exit. With #49647, every replication request was now waiting for the index to appear, which was problematic when the given index had just been deleted (e.g. deleting a follower index while it's still receiving requests from the leader, where these requests would now wait up to a minute for the index to appear). This PR now adds b3697cc on top of that prior PR to make sure to reestablish some of the prior behavior where the reroute phase waits for the bulk request for the index to appear. That logic was in place to ensure that when an index was created and not all nodes had learned about it yet, that the bulk would not fail somewhere in the reroute phase. This is now only restricted to the situation where the current node has an older cluster state than the one that coordinated the bulk request (which checks that the index is present). This also means that when an index is deleted, we will no longer unnecessarily wait up to the timeout for the index o appear, and instead fail the request. Closes #20279 --- ...TransportVerifyShardBeforeCloseAction.java | 5 +- .../flush/TransportShardFlushAction.java | 5 +- .../refresh/TransportShardRefreshAction.java | 5 +- .../action/bulk/BulkShardRequest.java | 5 ++ .../action/bulk/TransportBulkAction.java | 1 + .../action/bulk/TransportShardBulkAction.java | 11 +-- .../TransportResyncReplicationAction.java | 6 +- .../replication/ReplicationRequest.java | 2 +- .../TransportReplicationAction.java | 89 ++++++++----------- .../replication/TransportWriteAction.java | 6 +- .../seqno/GlobalCheckpointSyncAction.java | 5 +- .../RetentionLeaseBackgroundSyncAction.java | 5 +- .../index/seqno/RetentionLeaseSyncAction.java | 5 +- ...portVerifyShardBeforeCloseActionTests.java | 3 +- .../action/bulk/BulkIntegrationIT.java | 43 +++++++++ ...TransportResyncReplicationActionTests.java | 4 +- ...tReplicationActionRetryOnClosedNodeIT.java | 5 +- .../TransportReplicationActionTests.java | 33 ++++--- ...ReplicationAllPermitsAcquisitionTests.java | 3 +- .../TransportWriteActionTests.java | 7 +- .../GlobalCheckpointSyncActionTests.java | 4 +- ...tentionLeaseBackgroundSyncActionTests.java | 10 +-- .../seqno/RetentionLeaseSyncActionTests.java | 10 +-- .../indices/cluster/ClusterStateChanges.java | 2 +- .../snapshots/SnapshotResiliencyTests.java | 7 +- .../TransportBulkShardOperationsAction.java | 5 +- .../elasticsearch/xpack/CcrIntegTestCase.java | 2 +- 27 files changed, 144 insertions(+), 144 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseAction.java index bbd55e27e61fd..348855b7252a5 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseAction.java @@ -32,7 +32,6 @@ import org.elasticsearch.cluster.action.shard.ShardStateAction; import org.elasticsearch.cluster.block.ClusterBlock; import org.elasticsearch.cluster.block.ClusterBlocks; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; @@ -60,8 +59,8 @@ public class TransportVerifyShardBeforeCloseAction extends TransportReplicationA public TransportVerifyShardBeforeCloseAction(final Settings settings, final TransportService transportService, final ClusterService clusterService, final IndicesService indicesService, final ThreadPool threadPool, final ShardStateAction stateAction, - final ActionFilters actionFilters, final IndexNameExpressionResolver resolver) { - super(settings, NAME, transportService, clusterService, indicesService, threadPool, stateAction, actionFilters, resolver, + final ActionFilters actionFilters) { + super(settings, NAME, transportService, clusterService, indicesService, threadPool, stateAction, actionFilters, ShardRequest::new, ShardRequest::new, ThreadPool.Names.MANAGEMENT); } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/flush/TransportShardFlushAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/flush/TransportShardFlushAction.java index 4b01329e8b4ff..077657cc62dd4 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/flush/TransportShardFlushAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/flush/TransportShardFlushAction.java @@ -25,7 +25,6 @@ import org.elasticsearch.action.support.replication.ReplicationResponse; import org.elasticsearch.action.support.replication.TransportReplicationAction; import org.elasticsearch.cluster.action.shard.ShardStateAction; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; @@ -46,9 +45,9 @@ public class TransportShardFlushAction @Inject public TransportShardFlushAction(Settings settings, TransportService transportService, ClusterService clusterService, IndicesService indicesService, ThreadPool threadPool, ShardStateAction shardStateAction, - ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver) { + ActionFilters actionFilters) { super(settings, NAME, transportService, clusterService, indicesService, threadPool, shardStateAction, - actionFilters, indexNameExpressionResolver, ShardFlushRequest::new, ShardFlushRequest::new, ThreadPool.Names.FLUSH); + actionFilters, ShardFlushRequest::new, ShardFlushRequest::new, ThreadPool.Names.FLUSH); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/refresh/TransportShardRefreshAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/refresh/TransportShardRefreshAction.java index dd981aa995992..a0ce70503958c 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/refresh/TransportShardRefreshAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/refresh/TransportShardRefreshAction.java @@ -26,7 +26,6 @@ import org.elasticsearch.action.support.replication.ReplicationResponse; import org.elasticsearch.action.support.replication.TransportReplicationAction; import org.elasticsearch.cluster.action.shard.ShardStateAction; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; @@ -48,9 +47,9 @@ public class TransportShardRefreshAction @Inject public TransportShardRefreshAction(Settings settings, TransportService transportService, ClusterService clusterService, IndicesService indicesService, ThreadPool threadPool, ShardStateAction shardStateAction, - ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver) { + ActionFilters actionFilters) { super(settings, NAME, transportService, clusterService, indicesService, threadPool, shardStateAction, actionFilters, - indexNameExpressionResolver, BasicReplicationRequest::new, BasicReplicationRequest::new, ThreadPool.Names.REFRESH); + BasicReplicationRequest::new, BasicReplicationRequest::new, ThreadPool.Names.REFRESH); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequest.java b/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequest.java index 29e00c20446bb..0f05f071ad4f3 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequest.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequest.java @@ -118,6 +118,11 @@ public String getDescription() { return stringBuilder.toString(); } + @Override + protected BulkShardRequest routedBasedOnClusterVersion(long routedBasedOnClusterVersion) { + return super.routedBasedOnClusterVersion(routedBasedOnClusterVersion); + } + @Override public void onRetry() { for (BulkItemRequest item : items) { diff --git a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java index f21ab304c49ec..6a8912cc1dc82 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java @@ -486,6 +486,7 @@ protected void doRun() { requests.toArray(new BulkItemRequest[requests.size()])); bulkShardRequest.waitForActiveShards(bulkRequest.waitForActiveShards()); bulkShardRequest.timeout(bulkRequest.timeout()); + bulkShardRequest.routedBasedOnClusterVersion(clusterState.version()); if (task != null) { bulkShardRequest.setParentTask(nodeId, task.getId()); } diff --git a/server/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java b/server/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java index c322af9a9d3e5..e709c24bc64c7 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java @@ -44,7 +44,6 @@ import org.elasticsearch.cluster.action.index.MappingUpdatedAction; import org.elasticsearch.cluster.action.shard.ShardStateAction; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.MappingMetaData; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.bytes.BytesReference; @@ -93,10 +92,9 @@ public class TransportShardBulkAction extends TransportWriteAction> listener) { diff --git a/server/src/main/java/org/elasticsearch/action/resync/TransportResyncReplicationAction.java b/server/src/main/java/org/elasticsearch/action/resync/TransportResyncReplicationAction.java index 095f5e27a62fb..94dde512aa105 100644 --- a/server/src/main/java/org/elasticsearch/action/resync/TransportResyncReplicationAction.java +++ b/server/src/main/java/org/elasticsearch/action/resync/TransportResyncReplicationAction.java @@ -27,7 +27,6 @@ import org.elasticsearch.action.support.replication.TransportWriteAction; import org.elasticsearch.cluster.action.shard.ShardStateAction; import org.elasticsearch.cluster.block.ClusterBlockLevel; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; @@ -55,10 +54,9 @@ public class TransportResyncReplicationAction extends TransportWriteAction requestReader, + ActionFilters actionFilters, Writeable.Reader requestReader, Writeable.Reader replicaRequestReader, String executor) { this(settings, actionName, transportService, clusterService, indicesService, threadPool, shardStateAction, actionFilters, - indexNameExpressionResolver, requestReader, replicaRequestReader, executor, false, false); + requestReader, replicaRequestReader, executor, false, false); } protected TransportReplicationAction(Settings settings, String actionName, TransportService transportService, ClusterService clusterService, IndicesService indicesService, ThreadPool threadPool, ShardStateAction shardStateAction, - ActionFilters actionFilters, - IndexNameExpressionResolver indexNameExpressionResolver, Writeable.Reader requestReader, + ActionFilters actionFilters, Writeable.Reader requestReader, Writeable.Reader replicaRequestReader, String executor, boolean syncGlobalCheckpointAfterOperation, boolean forceExecutionOnPrimary) { super(actionName, actionFilters, transportService.getTaskManager()); @@ -138,7 +133,6 @@ protected TransportReplicationAction(Settings settings, String actionName, Trans this.clusterService = clusterService; this.indicesService = indicesService; this.shardStateAction = shardStateAction; - this.indexNameExpressionResolver = indexNameExpressionResolver; this.executor = executor; this.transportPrimaryAction = actionName + "[p]"; @@ -219,21 +213,10 @@ public ClusterBlockLevel indexBlockLevel() { return null; } - /** - * True if provided index should be resolved when resolving request - */ - protected boolean resolveIndex() { - return true; - } - protected TransportRequestOptions transportOptions(Settings settings) { return TransportRequestOptions.EMPTY; } - private String concreteIndex(final ClusterState state, final ReplicationRequest request) { - return resolveIndex() ? indexNameExpressionResolver.concreteSingleIndex(state, request).getName() : request.index(); - } - private ClusterBlockException blockExceptions(final ClusterState state, final String indexName) { ClusterBlockLevel globalBlockLevel = globalBlockLevel(); if (globalBlockLevel != null) { @@ -648,8 +631,7 @@ public void onFailure(Exception e) { protected void doRun() { setPhase(task, "routing"); final ClusterState state = observer.setAndGetObservedState(); - final String concreteIndex = concreteIndex(state, request); - final ClusterBlockException blockException = blockExceptions(state, concreteIndex); + final ClusterBlockException blockException = blockExceptions(state, request.shardId().getIndexName()); if (blockException != null) { if (blockException.retryable()) { logger.trace("cluster is blocked, scheduling a retry", blockException); @@ -658,23 +640,47 @@ protected void doRun() { finishAsFailed(blockException); } } else { - // request does not have a shardId yet, we need to pass the concrete index to resolve shardId - final IndexMetaData indexMetaData = state.metaData().index(concreteIndex); + final IndexMetaData indexMetaData = state.metaData().index(request.shardId().getIndex()); if (indexMetaData == null) { - retry(new IndexNotFoundException(concreteIndex)); - return; + // ensure that the cluster state on the node is at least as high as the node that decided that the index was there + if (state.version() < request.routedBasedOnClusterVersion()) { + logger.trace("failed to find index [{}] for request [{}] despite sender thinking it would be here. " + + "Local cluster state version [{}]] is older than on sending node (version [{}]), scheduling a retry...", + request.shardId().getIndex(), request, state.version(), request.routedBasedOnClusterVersion()); + retry(new IndexNotFoundException("failed to find index as current cluster state with version [" + state.version() + + "] is stale (expected at least [" + request.routedBasedOnClusterVersion() + "]", + request.shardId().getIndexName())); + return; + } else { + finishAsFailed(new IndexNotFoundException(request.shardId().getIndex())); + return; + } } + if (indexMetaData.getState() == IndexMetaData.State.CLOSE) { - throw new IndexClosedException(indexMetaData.getIndex()); + finishAsFailed(new IndexClosedException(indexMetaData.getIndex())); + return; } - // resolve all derived request fields, so we can route and apply it - resolveRequest(indexMetaData, request); + if (request.waitForActiveShards() == ActiveShardCount.DEFAULT) { + // if the wait for active shard count has not been set in the request, + // resolve it from the index settings + request.waitForActiveShards(indexMetaData.getWaitForActiveShards()); + } assert request.waitForActiveShards() != ActiveShardCount.DEFAULT : "request waitForActiveShards must be set in resolveRequest"; - final ShardRouting primary = primary(state); - if (retryIfUnavailable(state, primary)) { + final ShardRouting primary = state.getRoutingTable().shardRoutingTable(request.shardId()).primaryShard(); + if (primary == null || primary.active() == false) { + logger.trace("primary shard [{}] is not yet active, scheduling a retry: action [{}], request [{}], " + + "cluster state version [{}]", request.shardId(), actionName, request, state.version()); + retryBecauseUnavailable(request.shardId(), "primary shard is not active"); + return; + } + if (state.nodes().nodeExists(primary.currentNodeId()) == false) { + logger.trace("primary shard [{}] is assigned to an unknown node [{}], scheduling a retry: action [{}], request [{}], " + + "cluster state version [{}]", request.shardId(), primary.currentNodeId(), actionName, request, state.version()); + retryBecauseUnavailable(request.shardId(), "primary shard isn't assigned to a known node."); return; } final DiscoveryNode node = state.nodes().get(primary.currentNodeId()); @@ -718,27 +724,6 @@ private void performRemoteAction(ClusterState state, ShardRouting primary, Disco performAction(node, actionName, false, request); } - private boolean retryIfUnavailable(ClusterState state, ShardRouting primary) { - if (primary == null || primary.active() == false) { - logger.trace("primary shard [{}] is not yet active, scheduling a retry: action [{}], request [{}], " - + "cluster state version [{}]", request.shardId(), actionName, request, state.version()); - retryBecauseUnavailable(request.shardId(), "primary shard is not active"); - return true; - } - if (state.nodes().nodeExists(primary.currentNodeId()) == false) { - logger.trace("primary shard [{}] is assigned to an unknown node [{}], scheduling a retry: action [{}], request [{}], " - + "cluster state version [{}]", request.shardId(), primary.currentNodeId(), actionName, request, state.version()); - retryBecauseUnavailable(request.shardId(), "primary shard isn't assigned to a known node."); - return true; - } - return false; - } - - private ShardRouting primary(ClusterState state) { - IndexShardRoutingTable indexShard = state.getRoutingTable().shardRoutingTable(request.shardId()); - return indexShard.primaryShard(); - } - private void performAction(final DiscoveryNode node, final String action, final boolean isPrimaryAction, final TransportRequest requestToPerform) { transportService.sendRequest(node, action, requestToPerform, transportOptions, new TransportResponseHandler() { diff --git a/server/src/main/java/org/elasticsearch/action/support/replication/TransportWriteAction.java b/server/src/main/java/org/elasticsearch/action/support/replication/TransportWriteAction.java index 86e2760c9012d..104d3517815c0 100644 --- a/server/src/main/java/org/elasticsearch/action/support/replication/TransportWriteAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/replication/TransportWriteAction.java @@ -28,7 +28,6 @@ import org.elasticsearch.action.support.WriteResponse; import org.elasticsearch.cluster.action.shard.ShardStateAction; import org.elasticsearch.cluster.block.ClusterBlockLevel; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Nullable; @@ -61,11 +60,10 @@ public abstract class TransportWriteAction< protected TransportWriteAction(Settings settings, String actionName, TransportService transportService, ClusterService clusterService, IndicesService indicesService, ThreadPool threadPool, - ShardStateAction shardStateAction, ActionFilters actionFilters, - IndexNameExpressionResolver indexNameExpressionResolver, Writeable.Reader request, + ShardStateAction shardStateAction, ActionFilters actionFilters, Writeable.Reader request, Writeable.Reader replicaRequest, String executor, boolean forceExecutionOnPrimary) { super(settings, actionName, transportService, clusterService, indicesService, threadPool, shardStateAction, actionFilters, - indexNameExpressionResolver, request, replicaRequest, executor, true, forceExecutionOnPrimary); + request, replicaRequest, executor, true, forceExecutionOnPrimary); } /** Syncs operation result to the translog or throws a shard not available failure */ diff --git a/server/src/main/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncAction.java b/server/src/main/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncAction.java index cd79aa9d60e2e..0cdecbf4306dc 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncAction.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncAction.java @@ -26,7 +26,6 @@ import org.elasticsearch.action.support.replication.ReplicationResponse; import org.elasticsearch.action.support.replication.TransportReplicationAction; import org.elasticsearch.cluster.action.shard.ShardStateAction; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; @@ -61,8 +60,7 @@ public GlobalCheckpointSyncAction( final IndicesService indicesService, final ThreadPool threadPool, final ShardStateAction shardStateAction, - final ActionFilters actionFilters, - final IndexNameExpressionResolver indexNameExpressionResolver) { + final ActionFilters actionFilters) { super( settings, ACTION_NAME, @@ -72,7 +70,6 @@ public GlobalCheckpointSyncAction( threadPool, shardStateAction, actionFilters, - indexNameExpressionResolver, Request::new, Request::new, ThreadPool.Names.MANAGEMENT); diff --git a/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncAction.java b/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncAction.java index e3d3fed4a5107..1f6dd80a9f82e 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncAction.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncAction.java @@ -32,7 +32,6 @@ import org.elasticsearch.action.support.replication.ReplicationTask; import org.elasticsearch.action.support.replication.TransportReplicationAction; import org.elasticsearch.cluster.action.shard.ShardStateAction; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; @@ -81,8 +80,7 @@ public RetentionLeaseBackgroundSyncAction( final IndicesService indicesService, final ThreadPool threadPool, final ShardStateAction shardStateAction, - final ActionFilters actionFilters, - final IndexNameExpressionResolver indexNameExpressionResolver) { + final ActionFilters actionFilters) { super( settings, ACTION_NAME, @@ -92,7 +90,6 @@ public RetentionLeaseBackgroundSyncAction( threadPool, shardStateAction, actionFilters, - indexNameExpressionResolver, Request::new, Request::new, ThreadPool.Names.MANAGEMENT); diff --git a/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeaseSyncAction.java b/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeaseSyncAction.java index b93deccd51474..44da33186983a 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeaseSyncAction.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeaseSyncAction.java @@ -34,7 +34,6 @@ import org.elasticsearch.action.support.replication.TransportWriteAction; import org.elasticsearch.cluster.action.shard.ShardStateAction; import org.elasticsearch.cluster.block.ClusterBlockLevel; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; @@ -78,8 +77,7 @@ public RetentionLeaseSyncAction( final IndicesService indicesService, final ThreadPool threadPool, final ShardStateAction shardStateAction, - final ActionFilters actionFilters, - final IndexNameExpressionResolver indexNameExpressionResolver) { + final ActionFilters actionFilters) { super( settings, ACTION_NAME, @@ -89,7 +87,6 @@ public RetentionLeaseSyncAction( threadPool, shardStateAction, actionFilters, - indexNameExpressionResolver, RetentionLeaseSyncAction.Request::new, RetentionLeaseSyncAction.Request::new, ThreadPool.Names.MANAGEMENT, false); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseActionTests.java index 6913b518d2464..ba48eabadcca4 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseActionTests.java @@ -33,7 +33,6 @@ import org.elasticsearch.cluster.block.ClusterBlock; import org.elasticsearch.cluster.block.ClusterBlocks; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.MetaDataIndexStateService; import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.ShardRouting; @@ -119,7 +118,7 @@ public void setUp() throws Exception { ShardStateAction shardStateAction = new ShardStateAction(clusterService, transportService, null, null, threadPool); action = new TransportVerifyShardBeforeCloseAction(Settings.EMPTY, transportService, clusterService, mock(IndicesService.class), - mock(ThreadPool.class), shardStateAction, mock(ActionFilters.class), mock(IndexNameExpressionResolver.class)); + mock(ThreadPool.class), shardStateAction, mock(ActionFilters.class)); } @Override diff --git a/server/src/test/java/org/elasticsearch/action/bulk/BulkIntegrationIT.java b/server/src/test/java/org/elasticsearch/action/bulk/BulkIntegrationIT.java index bbd3be0b87920..93603ac5b5e27 100644 --- a/server/src/test/java/org/elasticsearch/action/bulk/BulkIntegrationIT.java +++ b/server/src/test/java/org/elasticsearch/action/bulk/BulkIntegrationIT.java @@ -20,13 +20,16 @@ package org.elasticsearch.action.bulk; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.ingest.PutPipelineRequest; import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.action.support.replication.ReplicationRequest; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -43,12 +46,19 @@ import java.util.Collections; import java.util.Map; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +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.StreamsUtils.copyToStringFromClasspath; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.isOneOf; public class BulkIntegrationIT extends ESIntegTestCase { @Override @@ -156,4 +166,37 @@ private void createSamplePipeline(String pipelineId) throws IOException, Executi assertTrue(acknowledgedResponse.isAcknowledged()); } + + /** This test ensures that index deletion makes indexing fail quickly, not wait on the index that has disappeared */ + public void testDeleteIndexWhileIndexing() throws Exception { + String index = "deleted_while_indexing"; + createIndex(index); + AtomicBoolean stopped = new AtomicBoolean(); + Thread[] threads = new Thread[between(1, 4)]; + AtomicInteger docID = new AtomicInteger(); + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + while (stopped.get() == false && docID.get() < 5000) { + String id = Integer.toString(docID.incrementAndGet()); + try { + IndexResponse response = client().prepareIndex(index).setId(id) + .setSource(Map.of("f" + randomIntBetween(1, 10), randomNonNegativeLong()), XContentType.JSON).get(); + assertThat(response.getResult(), isOneOf(CREATED, UPDATED)); + logger.info("--> index id={} seq_no={}", response.getId(), response.getSeqNo()); + } catch (ElasticsearchException ignore) { + logger.info("--> fail to index id={}", id); + } + } + }); + threads[i].start(); + } + ensureGreen(index); + assertBusy(() -> assertThat(docID.get(), greaterThanOrEqualTo(1))); + assertAcked(client().admin().indices().prepareDelete(index)); + stopped.set(true); + for (Thread thread : threads) { + thread.join(ReplicationRequest.DEFAULT_TIMEOUT.millis() / 2); + assertFalse(thread.isAlive()); + } + } } diff --git a/server/src/test/java/org/elasticsearch/action/resync/TransportResyncReplicationActionTests.java b/server/src/test/java/org/elasticsearch/action/resync/TransportResyncReplicationActionTests.java index d54317f114328..752114832bff1 100644 --- a/server/src/test/java/org/elasticsearch/action/resync/TransportResyncReplicationActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/resync/TransportResyncReplicationActionTests.java @@ -27,7 +27,6 @@ import org.elasticsearch.cluster.block.ClusterBlocks; import org.elasticsearch.cluster.coordination.NoMasterBlockService; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.routing.ShardRoutingState; @@ -143,9 +142,8 @@ public void testResyncDoesNotBlockOnPrimaryAction() throws Exception { final IndicesService indexServices = mock(IndicesService.class); when(indexServices.indexServiceSafe(eq(index))).thenReturn(indexService); - final IndexNameExpressionResolver resolver = new IndexNameExpressionResolver(); final TransportResyncReplicationAction action = new TransportResyncReplicationAction(Settings.EMPTY, transportService, - clusterService, indexServices, threadPool, shardStateAction, new ActionFilters(new HashSet<>()), resolver); + clusterService, indexServices, threadPool, shardStateAction, new ActionFilters(new HashSet<>())); assertThat(action.globalBlockLevel(), nullValue()); assertThat(action.indexBlockLevel(), nullValue()); diff --git a/server/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionRetryOnClosedNodeIT.java b/server/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionRetryOnClosedNodeIT.java index 58564fee58c06..160e37f1e8f77 100644 --- a/server/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionRetryOnClosedNodeIT.java +++ b/server/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionRetryOnClosedNodeIT.java @@ -26,7 +26,6 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.cluster.action.shard.ShardStateAction; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -102,9 +101,9 @@ public static class TestAction extends TransportReplicationAction listener = new PlainActionFuture<>(); ReplicationTask task = maybeTask(); @@ -253,7 +254,7 @@ public ClusterBlockLevel indexBlockLevel() { { setStateWithBlock(clusterService, retryableBlock, globalBlock); - Request requestWithTimeout = (globalBlock ? new Request(NO_SHARD_ID) : new Request(NO_SHARD_ID).index("index")).timeout("5ms"); + Request requestWithTimeout = (globalBlock ? new Request(shardId) : new Request(shardId)).timeout("5ms"); PlainActionFuture listener = new PlainActionFuture<>(); ReplicationTask task = maybeTask(); @@ -269,7 +270,7 @@ public ClusterBlockLevel indexBlockLevel() { { setStateWithBlock(clusterService, retryableBlock, globalBlock); - Request request = globalBlock ? new Request(NO_SHARD_ID) : new Request(NO_SHARD_ID).index("index"); + Request request = new Request(shardId); PlainActionFuture listener = new PlainActionFuture<>(); ReplicationTask task = maybeTask(); @@ -487,6 +488,7 @@ public void testUnknownIndexOrShardOnReroute() { setState(clusterService, state(index, true, randomBoolean() ? ShardRoutingState.INITIALIZING : ShardRoutingState.UNASSIGNED)); logger.debug("--> using initial state:\n{}", clusterService.state()); + Request request = new Request(new ShardId("unknown_index", "_na_", 0)).timeout("1ms"); PlainActionFuture listener = new PlainActionFuture<>(); ReplicationTask task = maybeTask(); @@ -495,7 +497,21 @@ public void testUnknownIndexOrShardOnReroute() { reroutePhase.run(); assertListenerThrows("must throw index not found exception", listener, IndexNotFoundException.class); assertPhase(task, "failed"); + assertFalse(request.isRetrySet.get()); + + // try again with a request that is based on a newer cluster state, make sure we waited until that + // cluster state for the index to appear + request = new Request(new ShardId("unknown_index", "_na_", 0)).timeout("1ms"); + request.routedBasedOnClusterVersion(clusterService.state().version() + 1); + listener = new PlainActionFuture<>(); + task = maybeTask(); + + reroutePhase = action.new ReroutePhase(task, request, listener); + reroutePhase.run(); + assertListenerThrows("must throw index not found exception", listener, IndexNotFoundException.class); + assertPhase(task, "failed"); assertTrue(request.isRetrySet.get()); + request = new Request(new ShardId(index, "_na_", 10)).timeout("1ms"); listener = new PlainActionFuture<>(); reroutePhase = action.new ReroutePhase(null, request, listener); @@ -513,7 +529,7 @@ public void testClosedIndexOnReroute() { new CloseIndexRequest(index))); assertThat(clusterService.state().metaData().indices().get(index).getState(), equalTo(IndexMetaData.State.CLOSE)); logger.debug("--> using initial state:\n{}", clusterService.state()); - Request request = new Request(new ShardId("test", "_na_", 0)).timeout("1ms"); + Request request = new Request(new ShardId(clusterService.state().metaData().indices().get(index).getIndex(), 0)).timeout("1ms"); PlainActionFuture listener = new PlainActionFuture<>(); ReplicationTask task = maybeTask(); @@ -1245,7 +1261,7 @@ private class TestAction extends TransportReplicationAction()), new IndexNameExpressionResolver(), + new ActionFilters(new HashSet<>()), Request::new, Request::new, ThreadPool.Names.SAME); } @@ -1267,11 +1283,6 @@ protected ReplicaResult shardOperationOnReplica(Request request, IndexShard repl request.processedOnReplicas.incrementAndGet(); return new ReplicaResult(); } - - @Override - protected boolean resolveIndex() { - return false; - } } private IndicesService mockIndicesService(ClusterService clusterService) { diff --git a/server/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationAllPermitsAcquisitionTests.java b/server/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationAllPermitsAcquisitionTests.java index 2560191a706d5..bda6a68030f70 100644 --- a/server/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationAllPermitsAcquisitionTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationAllPermitsAcquisitionTests.java @@ -30,7 +30,6 @@ import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.block.ClusterBlocks; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodeRole; @@ -418,7 +417,7 @@ private abstract class TestAction extends TransportReplicationAction executedOnPrimary) { super(settings, actionName, transportService, clusterService, mockIndicesService(shardId, executedOnPrimary, primary, replica), threadPool, shardStateAction, - new ActionFilters(new HashSet<>()), new IndexNameExpressionResolver(), Request::new, Request::new, ThreadPool.Names.SAME); + new ActionFilters(new HashSet<>()), Request::new, Request::new, ThreadPool.Names.SAME); this.shardId = Objects.requireNonNull(shardId); this.primary = Objects.requireNonNull(primary); assertEquals(shardId, primary.shardId()); diff --git a/server/src/test/java/org/elasticsearch/action/support/replication/TransportWriteActionTests.java b/server/src/test/java/org/elasticsearch/action/support/replication/TransportWriteActionTests.java index e12151595d552..c22613b9c291f 100644 --- a/server/src/test/java/org/elasticsearch/action/support/replication/TransportWriteActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/replication/TransportWriteActionTests.java @@ -31,7 +31,6 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.action.shard.ShardStateAction; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.RoutingNode; import org.elasticsearch.cluster.routing.ShardRouting; @@ -399,8 +398,7 @@ protected TestAction(boolean withDocumentFailureOnPrimary, boolean withDocumentF super(Settings.EMPTY, "internal:test", new TransportService(Settings.EMPTY, mock(Transport.class), null, TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()), null, null, null, null, - new ActionFilters(new HashSet<>()), new IndexNameExpressionResolver(), TestRequest::new, - TestRequest::new, ThreadPool.Names.SAME, false); + new ActionFilters(new HashSet<>()), TestRequest::new, TestRequest::new, ThreadPool.Names.SAME, false); this.withDocumentFailureOnPrimary = withDocumentFailureOnPrimary; this.withDocumentFailureOnReplica = withDocumentFailureOnReplica; } @@ -409,8 +407,7 @@ protected TestAction(Settings settings, String actionName, TransportService tran ClusterService clusterService, ShardStateAction shardStateAction, ThreadPool threadPool) { super(settings, actionName, transportService, clusterService, mockIndicesService(clusterService), threadPool, shardStateAction, - new ActionFilters(new HashSet<>()), new IndexNameExpressionResolver(), - TestRequest::new, TestRequest::new, ThreadPool.Names.SAME, false); + new ActionFilters(new HashSet<>()), TestRequest::new, TestRequest::new, ThreadPool.Names.SAME, false); this.withDocumentFailureOnPrimary = false; this.withDocumentFailureOnReplica = false; } diff --git a/server/src/test/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncActionTests.java b/server/src/test/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncActionTests.java index 79b9b231b48d4..8a1dc44a3e3c7 100644 --- a/server/src/test/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncActionTests.java +++ b/server/src/test/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncActionTests.java @@ -20,7 +20,6 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.ActionTestUtils; import org.elasticsearch.cluster.action.shard.ShardStateAction; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.internal.io.IOUtils; @@ -110,8 +109,7 @@ public void testTranslogSyncAfterGlobalCheckpointSync() throws Exception { indicesService, threadPool, shardStateAction, - new ActionFilters(Collections.emptySet()), - new IndexNameExpressionResolver()); + new ActionFilters(Collections.emptySet())); final GlobalCheckpointSyncAction.Request primaryRequest = new GlobalCheckpointSyncAction.Request(indexShard.shardId()); if (randomBoolean()) { action.shardOperationOnPrimary(primaryRequest, indexShard, ActionTestUtils.assertNoFailureListener(r -> {})); diff --git a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncActionTests.java b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncActionTests.java index 2be10a80fe793..282170a58e3e3 100644 --- a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncActionTests.java +++ b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncActionTests.java @@ -23,7 +23,6 @@ import org.elasticsearch.action.support.ActionTestUtils; import org.elasticsearch.action.support.replication.TransportReplicationAction; import org.elasticsearch.cluster.action.shard.ShardStateAction; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.internal.io.IOUtils; @@ -106,8 +105,7 @@ public void testRetentionLeaseBackgroundSyncActionOnPrimary() throws Interrupted indicesService, threadPool, shardStateAction, - new ActionFilters(Collections.emptySet()), - new IndexNameExpressionResolver()); + new ActionFilters(Collections.emptySet())); final RetentionLeases retentionLeases = mock(RetentionLeases.class); final RetentionLeaseBackgroundSyncAction.Request request = new RetentionLeaseBackgroundSyncAction.Request(indexShard.shardId(), retentionLeases); @@ -144,8 +142,7 @@ public void testRetentionLeaseBackgroundSyncActionOnReplica() throws WriteStateE indicesService, threadPool, shardStateAction, - new ActionFilters(Collections.emptySet()), - new IndexNameExpressionResolver()); + new ActionFilters(Collections.emptySet())); final RetentionLeases retentionLeases = mock(RetentionLeases.class); final RetentionLeaseBackgroundSyncAction.Request request = new RetentionLeaseBackgroundSyncAction.Request(indexShard.shardId(), retentionLeases); @@ -182,8 +179,7 @@ public void testBlocks() { indicesService, threadPool, shardStateAction, - new ActionFilters(Collections.emptySet()), - new IndexNameExpressionResolver()); + new ActionFilters(Collections.emptySet())); assertNull(action.indexBlockLevel()); } diff --git a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseSyncActionTests.java b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseSyncActionTests.java index 14dbb1cc5eed8..a63722d93666a 100644 --- a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseSyncActionTests.java +++ b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseSyncActionTests.java @@ -22,7 +22,6 @@ import org.elasticsearch.action.support.ActionTestUtils; import org.elasticsearch.action.support.replication.TransportWriteAction; import org.elasticsearch.cluster.action.shard.ShardStateAction; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.internal.io.IOUtils; @@ -102,8 +101,7 @@ public void testRetentionLeaseSyncActionOnPrimary() { indicesService, threadPool, shardStateAction, - new ActionFilters(Collections.emptySet()), - new IndexNameExpressionResolver()); + new ActionFilters(Collections.emptySet())); final RetentionLeases retentionLeases = mock(RetentionLeases.class); final RetentionLeaseSyncAction.Request request = new RetentionLeaseSyncAction.Request(indexShard.shardId(), retentionLeases); action.shardOperationOnPrimary(request, indexShard, @@ -139,8 +137,7 @@ public void testRetentionLeaseSyncActionOnReplica() throws WriteStateException { indicesService, threadPool, shardStateAction, - new ActionFilters(Collections.emptySet()), - new IndexNameExpressionResolver()); + new ActionFilters(Collections.emptySet())); final RetentionLeases retentionLeases = mock(RetentionLeases.class); final RetentionLeaseSyncAction.Request request = new RetentionLeaseSyncAction.Request(indexShard.shardId(), retentionLeases); @@ -177,8 +174,7 @@ public void testBlocks() { indicesService, threadPool, shardStateAction, - new ActionFilters(Collections.emptySet()), - new IndexNameExpressionResolver()); + new ActionFilters(Collections.emptySet())); assertNull(action.indexBlockLevel()); } diff --git a/server/src/test/java/org/elasticsearch/indices/cluster/ClusterStateChanges.java b/server/src/test/java/org/elasticsearch/indices/cluster/ClusterStateChanges.java index 3d3ba3b8cf7b1..74b94ad3a16b4 100644 --- a/server/src/test/java/org/elasticsearch/indices/cluster/ClusterStateChanges.java +++ b/server/src/test/java/org/elasticsearch/indices/cluster/ClusterStateChanges.java @@ -194,7 +194,7 @@ public IndexMetaData upgradeIndexMetaData(IndexMetaData indexMetaData, Version m NodeClient client = new NodeClient(Settings.EMPTY, threadPool); Map actions = new HashMap<>(); actions.put(TransportVerifyShardBeforeCloseAction.TYPE, new TransportVerifyShardBeforeCloseAction(SETTINGS, - transportService, clusterService, indicesService, threadPool, null, actionFilters, indexNameExpressionResolver)); + transportService, clusterService, indicesService, threadPool, null, actionFilters)); client.initialize(actions, transportService.getTaskManager(), null, null); MetaDataIndexStateService indexStateService = new MetaDataIndexStateService(clusterService, allocationService, diff --git a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java index 124bf290677bb..28d2beb2efcc4 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java @@ -1108,7 +1108,7 @@ public void onFailure(final Exception e) { Map actions = new HashMap<>(); actions.put(GlobalCheckpointSyncAction.TYPE, new GlobalCheckpointSyncAction(settings, transportService, clusterService, indicesService, - threadPool, shardStateAction, actionFilters, indexNameExpressionResolver)); + threadPool, shardStateAction, actionFilters)); final MetaDataMappingService metaDataMappingService = new MetaDataMappingService(clusterService, indicesService); indicesClusterStateService = new IndicesClusterStateService( settings, @@ -1132,8 +1132,7 @@ public void onFailure(final Exception e) { indicesService, threadPool, shardStateAction, - actionFilters, - indexNameExpressionResolver)), + actionFilters)), RetentionLeaseSyncer.EMPTY, client); final MetaDataCreateIndexService metaDataCreateIndexService = new MetaDataCreateIndexService(settings, clusterService, @@ -1159,7 +1158,7 @@ allocationService, new AliasValidator(), environment, indexScopedSettings, )); final TransportShardBulkAction transportShardBulkAction = new TransportShardBulkAction(settings, transportService, clusterService, indicesService, threadPool, shardStateAction, mappingUpdatedAction, new UpdateHelper(scriptService), - actionFilters, indexNameExpressionResolver); + actionFilters); actions.put(TransportShardBulkAction.TYPE, transportShardBulkAction); final RestoreService restoreService = new RestoreService( clusterService, repositoriesService, allocationService, diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/bulk/TransportBulkShardOperationsAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/bulk/TransportBulkShardOperationsAction.java index 984e915b175f6..8c4374fac3629 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/bulk/TransportBulkShardOperationsAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/bulk/TransportBulkShardOperationsAction.java @@ -12,7 +12,6 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.replication.TransportWriteAction; import org.elasticsearch.cluster.action.shard.ShardStateAction; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.inject.Inject; @@ -44,8 +43,7 @@ public TransportBulkShardOperationsAction( final IndicesService indicesService, final ThreadPool threadPool, final ShardStateAction shardStateAction, - final ActionFilters actionFilters, - final IndexNameExpressionResolver indexNameExpressionResolver) { + final ActionFilters actionFilters) { super( settings, BulkShardOperationsAction.NAME, @@ -55,7 +53,6 @@ public TransportBulkShardOperationsAction( threadPool, shardStateAction, actionFilters, - indexNameExpressionResolver, BulkShardOperationsRequest::new, BulkShardOperationsRequest::new, ThreadPool.Names.WRITE, false); diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java index 873bce2a50f57..cc48c080ac8ce 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java @@ -401,7 +401,7 @@ protected void ensureNoCcrTasks() throws Exception { numNodeTasks++; } } - assertThat(numNodeTasks, equalTo(0)); + assertThat(listTasksResponse.getTasks().toString(), numNodeTasks, equalTo(0)); }, 30, TimeUnit.SECONDS); } From b7bac252ba10d2747ed775cc7b2e5008026b3859 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Fri, 29 Nov 2019 10:32:57 +0000 Subject: [PATCH 022/686] Fix BWC assertion in GetFieldMappingsResponse (#49706) Closes #49702 --- .../indices/mapping/get/GetFieldMappingsResponse.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetFieldMappingsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetFieldMappingsResponse.java index bd4fbc5f82af5..c57ebbb942f68 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetFieldMappingsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetFieldMappingsResponse.java @@ -36,6 +36,7 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -66,7 +67,11 @@ public class GetFieldMappingsResponse extends ActionResponse implements ToXConte String index = in.readString(); if (in.getVersion().before(Version.V_8_0_0)) { int typesSize = in.readVInt(); - assert typesSize == 1; + assert typesSize == 1 || typesSize == 0 : "Expected 0 or 1 types but got " + typesSize; + if (typesSize == 0) { + indexMapBuilder.put(index, Collections.emptyMap()); + continue; + } in.readString(); // type } int fieldSize = in.readVInt(); From cc54d9306b04cda127bc2f89eb94eeb8c1b1ec9d Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Fri, 29 Nov 2019 11:39:51 +0000 Subject: [PATCH 023/686] New setting to prevent automatically importing dangling indices (#49174) Introduce a new static setting, `gateway.auto_import_dangling_indices`, which prevents dangling indices from being automatically imported. Part of #48366. --- .../common/settings/ClusterSettings.java | 2 + .../gateway/DanglingIndicesState.java | 24 ++++- .../gateway/DanglingIndicesStateTests.java | 71 +++++++++++++- .../indices/recovery/DanglingIndicesIT.java | 95 +++++++++++++++++++ .../indices/recovery/IndexRecoveryIT.java | 1 - .../test/InternalTestCluster.java | 2 +- 6 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/indices/recovery/DanglingIndicesIT.java diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 4548448719a68..ef40f05a2f261 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -70,6 +70,7 @@ import org.elasticsearch.discovery.SettingsBasedSeedHostsProvider; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; +import org.elasticsearch.gateway.DanglingIndicesState; import org.elasticsearch.gateway.GatewayService; import org.elasticsearch.gateway.IncrementalClusterStateWriter; import org.elasticsearch.http.HttpTransportSettings; @@ -186,6 +187,7 @@ public void apply(Settings value, Settings current, Settings previous) { BalancedShardsAllocator.THRESHOLD_SETTING, ClusterRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ALLOW_REBALANCE_SETTING, ConcurrentRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_CLUSTER_CONCURRENT_REBALANCE_SETTING, + DanglingIndicesState.AUTO_IMPORT_DANGLING_INDICES_SETTING, EnableAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ENABLE_SETTING, EnableAllocationDecider.CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING, FilterAllocationDecider.CLUSTER_ROUTING_INCLUDE_GROUP_SETTING, diff --git a/server/src/main/java/org/elasticsearch/gateway/DanglingIndicesState.java b/server/src/main/java/org/elasticsearch/gateway/DanglingIndicesState.java index d649c02af4e77..701a85fd90e50 100644 --- a/server/src/main/java/org/elasticsearch/gateway/DanglingIndicesState.java +++ b/server/src/main/java/org/elasticsearch/gateway/DanglingIndicesState.java @@ -30,6 +30,7 @@ import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.Index; @@ -55,9 +56,17 @@ public class DanglingIndicesState implements ClusterStateListener { private static final Logger logger = LogManager.getLogger(DanglingIndicesState.class); + public static final Setting AUTO_IMPORT_DANGLING_INDICES_SETTING = Setting.boolSetting( + "gateway.auto_import_dangling_indices", + true, + Setting.Property.NodeScope, + Setting.Property.Deprecated + ); + private final NodeEnvironment nodeEnv; private final MetaStateService metaStateService; private final LocalAllocateDangledIndices allocateDangledIndices; + private final boolean isAutoImportDanglingIndicesEnabled; private final Map danglingIndices = ConcurrentCollections.newConcurrentMap(); @@ -67,7 +76,18 @@ public DanglingIndicesState(NodeEnvironment nodeEnv, MetaStateService metaStateS this.nodeEnv = nodeEnv; this.metaStateService = metaStateService; this.allocateDangledIndices = allocateDangledIndices; - clusterService.addListener(this); + + this.isAutoImportDanglingIndicesEnabled = AUTO_IMPORT_DANGLING_INDICES_SETTING.get(clusterService.getSettings()); + + if (this.isAutoImportDanglingIndicesEnabled) { + clusterService.addListener(this); + } else { + logger.warn(AUTO_IMPORT_DANGLING_INDICES_SETTING.getKey() + " is disabled, dangling indices will not be detected or imported"); + } + } + + boolean isAutoImportDanglingIndicesEnabled() { + return this.isAutoImportDanglingIndicesEnabled; } /** @@ -171,7 +191,7 @@ private IndexMetaData stripAliases(IndexMetaData indexMetaData) { * Allocates the provided list of the dangled indices by sending them to the master node * for allocation. */ - private void allocateDanglingIndices() { + void allocateDanglingIndices() { if (danglingIndices.isEmpty()) { return; } diff --git a/server/src/test/java/org/elasticsearch/gateway/DanglingIndicesStateTests.java b/server/src/test/java/org/elasticsearch/gateway/DanglingIndicesStateTests.java index e7dfbadeeda78..42c546e192812 100644 --- a/server/src/test/java/org/elasticsearch/gateway/DanglingIndicesStateTests.java +++ b/server/src/test/java/org/elasticsearch/gateway/DanglingIndicesStateTests.java @@ -35,8 +35,12 @@ import java.nio.file.StandardCopyOption; import java.util.Map; +import static org.elasticsearch.gateway.DanglingIndicesState.AUTO_IMPORT_DANGLING_INDICES_SETTING; import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class DanglingIndicesStateTests extends ESTestCase { @@ -46,6 +50,13 @@ public class DanglingIndicesStateTests extends ESTestCase { .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) .build(); + // The setting AUTO_IMPORT_DANGLING_INDICES_SETTING is deprecated, so we must disable + // warning checks or all the tests will fail. + @Override + protected boolean enableWarningsCheck() { + return false; + } + public void testCleanupWhenEmpty() throws Exception { try (NodeEnvironment env = newNodeEnvironment()) { MetaStateService metaStateService = new MetaStateService(env, xContentRegistry()); @@ -57,11 +68,11 @@ public void testCleanupWhenEmpty() throws Exception { assertTrue(danglingState.getDanglingIndices().isEmpty()); } } + public void testDanglingIndicesDiscovery() throws Exception { try (NodeEnvironment env = newNodeEnvironment()) { MetaStateService metaStateService = new MetaStateService(env, xContentRegistry()); DanglingIndicesState danglingState = createDanglingIndicesState(env, metaStateService); - assertTrue(danglingState.getDanglingIndices().isEmpty()); MetaData metaData = MetaData.builder().build(); final Settings.Builder settings = Settings.builder().put(indexSettings).put(IndexMetaData.SETTING_INDEX_UUID, "test1UUID"); @@ -155,7 +166,6 @@ public void testDanglingIndicesNotImportedWhenTombstonePresent() throws Exceptio final IndexGraveyard graveyard = IndexGraveyard.builder().addTombstone(dangledIndex.getIndex()).build(); final MetaData metaData = MetaData.builder().indexGraveyard(graveyard).build(); assertThat(danglingState.findNewDanglingIndices(metaData).size(), equalTo(0)); - } } @@ -181,7 +191,62 @@ public void testDanglingIndicesStripAliases() throws Exception { } } + public void testDanglingIndicesAreNotAllocatedWhenDisabled() throws Exception { + try (NodeEnvironment env = newNodeEnvironment()) { + MetaStateService metaStateService = new MetaStateService(env, xContentRegistry()); + LocalAllocateDangledIndices localAllocateDangledIndices = mock(LocalAllocateDangledIndices.class); + + final Settings allocateSettings = Settings.builder().put(AUTO_IMPORT_DANGLING_INDICES_SETTING.getKey(), false).build(); + + final ClusterService clusterServiceMock = mock(ClusterService.class); + when(clusterServiceMock.getSettings()).thenReturn(allocateSettings); + + final DanglingIndicesState danglingIndicesState = new DanglingIndicesState( + env, + metaStateService, + localAllocateDangledIndices, + clusterServiceMock + ); + + assertFalse("Expected dangling imports to be disabled", danglingIndicesState.isAutoImportDanglingIndicesEnabled()); + } + } + + public void testDanglingIndicesAreAllocatedWhenEnabled() throws Exception { + try (NodeEnvironment env = newNodeEnvironment()) { + MetaStateService metaStateService = new MetaStateService(env, xContentRegistry()); + LocalAllocateDangledIndices localAllocateDangledIndices = mock(LocalAllocateDangledIndices.class); + final Settings allocateSettings = Settings.builder().put(AUTO_IMPORT_DANGLING_INDICES_SETTING.getKey(), true).build(); + + final ClusterService clusterServiceMock = mock(ClusterService.class); + when(clusterServiceMock.getSettings()).thenReturn(allocateSettings); + + DanglingIndicesState danglingIndicesState = new DanglingIndicesState( + env, + metaStateService, + localAllocateDangledIndices, clusterServiceMock + ); + + assertTrue("Expected dangling imports to be enabled", danglingIndicesState.isAutoImportDanglingIndicesEnabled()); + + final Settings.Builder settings = Settings.builder().put(indexSettings).put(IndexMetaData.SETTING_INDEX_UUID, "test1UUID"); + IndexMetaData dangledIndex = IndexMetaData.builder("test1").settings(settings).build(); + metaStateService.writeIndex("test_write", dangledIndex); + + danglingIndicesState.findNewAndAddDanglingIndices(MetaData.builder().build()); + + danglingIndicesState.allocateDanglingIndices(); + + verify(localAllocateDangledIndices).allocateDangled(any(), any()); + } + } + private DanglingIndicesState createDanglingIndicesState(NodeEnvironment env, MetaStateService metaStateService) { - return new DanglingIndicesState(env, metaStateService, null, mock(ClusterService.class)); + final Settings allocateSettings = Settings.builder().put(AUTO_IMPORT_DANGLING_INDICES_SETTING.getKey(), true).build(); + + final ClusterService clusterServiceMock = mock(ClusterService.class); + when(clusterServiceMock.getSettings()).thenReturn(allocateSettings); + + return new DanglingIndicesState(env, metaStateService, null, clusterServiceMock); } } diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/DanglingIndicesIT.java b/server/src/test/java/org/elasticsearch/indices/recovery/DanglingIndicesIT.java new file mode 100644 index 0000000000000..31afef73ad450 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/indices/recovery/DanglingIndicesIT.java @@ -0,0 +1,95 @@ +/* + * 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. + */ + +package org.elasticsearch.indices.recovery; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.ESIntegTestCase.ClusterScope; +import org.elasticsearch.test.InternalTestCluster; + +import java.util.concurrent.TimeUnit; + +import static org.elasticsearch.cluster.metadata.IndexGraveyard.SETTING_MAX_TOMBSTONES; +import static org.elasticsearch.gateway.DanglingIndicesState.AUTO_IMPORT_DANGLING_INDICES_SETTING; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; + +@ClusterScope(numDataNodes = 0, scope = ESIntegTestCase.Scope.TEST) +public class DanglingIndicesIT extends ESIntegTestCase { + private static final String INDEX_NAME = "test-idx-1"; + + private Settings buildSettings(boolean importDanglingIndices) { + return Settings.builder() + // Don't keep any indices in the graveyard, so that when we delete an index, + // it's definitely considered to be dangling. + .put(SETTING_MAX_TOMBSTONES.getKey(), 0) + .put(AUTO_IMPORT_DANGLING_INDICES_SETTING.getKey(), importDanglingIndices) + .build(); + } + + /** + * Check that when dangling indices are discovered, then they are recovered into + * the cluster, so long as the recovery setting is enabled. + */ + public void testDanglingIndicesAreRecoveredWhenSettingIsEnabled() throws Exception { + final Settings settings = buildSettings(true); + internalCluster().startNodes(3, settings); + + createIndex(INDEX_NAME, Settings.builder().put("number_of_replicas", 2).build()); + + // Restart node, deleting the index in its absence, so that there is a dangling index to recover + internalCluster().restartRandomDataNode(new InternalTestCluster.RestartCallback() { + + @Override + public Settings onNodeStopped(String nodeName) throws Exception { + assertAcked(client().admin().indices().prepareDelete(INDEX_NAME)); + return super.onNodeStopped(nodeName); + } + }); + + assertBusy(() -> assertTrue("Expected dangling index " + INDEX_NAME + " to be recovered", indexExists(INDEX_NAME))); + } + + /** + * Check that when dangling indices are discovered, then they are not recovered into + * the cluster when the recovery setting is disabled. + */ + public void testDanglingIndicesAreNotRecoveredWhenSettingIsDisabled() throws Exception { + internalCluster().startNodes(3, buildSettings(false)); + + createIndex(INDEX_NAME, Settings.builder().put("number_of_replicas", 2).build()); + + // Restart node, deleting the index in its absence, so that there is a dangling index to recover + internalCluster().restartRandomDataNode(new InternalTestCluster.RestartCallback() { + + @Override + public Settings onNodeStopped(String nodeName) throws Exception { + assertAcked(client().admin().indices().prepareDelete(INDEX_NAME)); + return super.onNodeStopped(nodeName); + } + }); + + // Since index recovery is async, we can't prove index recovery will never occur, just that it doesn't occur within some reasonable + // amount of time + assertFalse( + "Did not expect dangling index " + INDEX_NAME + " to be recovered", + waitUntil(() -> indexExists(INDEX_NAME), 1, TimeUnit.SECONDS) + ); + } +} diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java b/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java index 650afbbc9ba4d..414df8d648c51 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java @@ -142,7 +142,6 @@ public class IndexRecoveryIT extends ESIntegTestCase { private static final String INDEX_NAME = "test-idx-1"; - private static final String INDEX_TYPE = "test-type-1"; private static final String REPO_NAME = "test-repo-1"; private static final String SNAP_NAME = "test-snap-1"; diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java index ad88d88d4941f..9b609040379eb 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java @@ -1487,7 +1487,7 @@ public synchronized boolean stopRandomDataNode() throws IOException { } /** - * Stops a random node in the cluster that applies to the given filter or non if the non of the nodes applies to the + * Stops a random node in the cluster that applies to the given filter. Does nothing if none of the nodes match the * filter. */ public synchronized void stopRandomNode(final Predicate filter) throws IOException { From aaed7001aea8fc01a506b09766043d899c42db5d Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 29 Nov 2019 14:20:31 +0200 Subject: [PATCH 024/686] [ML] Add optional source filtering during data frame reindexing (#49690) This adds a `_source` setting under the `source` setting of a data frame analytics config. The new `_source` is reusing the structure of a `FetchSourceContext` like `analyzed_fields` does. Specifying includes and excludes for source allows selecting which fields will get reindexed and will be available in the destination index. Closes #49531 --- .../dataframe/DataFrameAnalyticsSource.java | 30 +++- .../MlClientDocumentationIT.java | 3 + .../DataFrameAnalyticsSourceTests.java | 9 ++ .../ml/put-data-frame-analytics.asciidoc | 1 + .../apis/dfanalyticsresources.asciidoc | 32 ++-- .../apis/put-dfanalytics.asciidoc | 53 +++--- .../action/PutDataFrameAnalyticsAction.java | 20 +++ .../dataframe/DataFrameAnalyticsConfig.java | 2 +- .../dataframe/DataFrameAnalyticsSource.java | 86 +++++++++- .../persistence/ElasticsearchMappings.java | 3 + .../ml/job/results/ReservedFieldNames.java | 1 + ...tDataFrameAnalyticsActionRequestTests.java | 44 +++++ .../DataFrameAnalyticsSourceTests.java | 73 ++++++++- .../ml/qa/ml-with-security/build.gradle | 1 + .../ExplainDataFrameAnalyticsIT.java | 3 +- ...NativeDataFrameAnalyticsIntegTestCase.java | 2 +- .../integration/RunDataFrameAnalyticsIT.java | 6 +- ...ransportStartDataFrameAnalyticsAction.java | 2 +- .../ml/dataframe/DataFrameAnalyticsIndex.java | 6 +- .../dataframe/DataFrameAnalyticsManager.java | 1 + .../xpack/ml/dataframe/MappingsMerger.java | 26 +-- .../extractor/ExtractedFieldsDetector.java | 11 ++ .../DataFrameAnalyticsIndexTests.java | 2 +- .../ml/dataframe/MappingsMergerTests.java | 46 +++++- .../dataframe/SourceDestValidatorTests.java | 2 +- .../ExtractedFieldsDetectorTests.java | 153 +++++++++++------- .../AnalyticsResultProcessorTests.java | 2 +- .../test/ml/data_frame_analytics_crud.yml | 28 +++- 28 files changed, 521 insertions(+), 127 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSource.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSource.java index 9a6de159bea3e..1f731f4c28aaa 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSource.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSource.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import java.io.IOException; import java.util.Arrays; @@ -44,20 +45,27 @@ public static Builder builder() { private static final ParseField INDEX = new ParseField("index"); private static final ParseField QUERY = new ParseField("query"); + public static final ParseField _SOURCE = new ParseField("_source"); private static ObjectParser PARSER = new ObjectParser<>("data_frame_analytics_source", true, Builder::new); static { PARSER.declareStringArray(Builder::setIndex, INDEX); PARSER.declareObject(Builder::setQueryConfig, (p, c) -> QueryConfig.fromXContent(p), QUERY); + PARSER.declareField(Builder::setSourceFiltering, + (p, c) -> FetchSourceContext.fromXContent(p), + _SOURCE, + ObjectParser.ValueType.OBJECT_ARRAY_BOOLEAN_OR_STRING); } private final String[] index; private final QueryConfig queryConfig; + private final FetchSourceContext sourceFiltering; - private DataFrameAnalyticsSource(String[] index, @Nullable QueryConfig queryConfig) { + private DataFrameAnalyticsSource(String[] index, @Nullable QueryConfig queryConfig, @Nullable FetchSourceContext sourceFiltering) { this.index = Objects.requireNonNull(index); this.queryConfig = queryConfig; + this.sourceFiltering = sourceFiltering; } public String[] getIndex() { @@ -68,6 +76,10 @@ public QueryConfig getQueryConfig() { return queryConfig; } + public FetchSourceContext getSourceFiltering() { + return sourceFiltering; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -75,6 +87,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (queryConfig != null) { builder.field(QUERY.getPreferredName(), queryConfig.getQuery()); } + if (sourceFiltering != null) { + builder.field(_SOURCE.getPreferredName(), sourceFiltering); + } builder.endObject(); return builder; } @@ -86,12 +101,13 @@ public boolean equals(Object o) { DataFrameAnalyticsSource other = (DataFrameAnalyticsSource) o; return Arrays.equals(index, other.index) - && Objects.equals(queryConfig, other.queryConfig); + && Objects.equals(queryConfig, other.queryConfig) + && Objects.equals(sourceFiltering, other.sourceFiltering); } @Override public int hashCode() { - return Objects.hash(Arrays.asList(index), queryConfig); + return Objects.hash(Arrays.asList(index), queryConfig, sourceFiltering); } @Override @@ -103,6 +119,7 @@ public static class Builder { private String[] index; private QueryConfig queryConfig; + private FetchSourceContext sourceFiltering; private Builder() {} @@ -121,8 +138,13 @@ public Builder setQueryConfig(QueryConfig queryConfig) { return this; } + public Builder setSourceFiltering(FetchSourceContext sourceFiltering) { + this.sourceFiltering = sourceFiltering; + return this; + } + public DataFrameAnalyticsSource build() { - return new DataFrameAnalyticsSource(index, queryConfig); + return new DataFrameAnalyticsSource(index, queryConfig, sourceFiltering); } } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 8c6e134822b60..1d9a151cf8ae3 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -2939,6 +2939,9 @@ public void testPutDataFrameAnalytics() throws Exception { DataFrameAnalyticsSource sourceConfig = DataFrameAnalyticsSource.builder() // <1> .setIndex("put-test-source-index") // <2> .setQueryConfig(queryConfig) // <3> + .setSourceFiltering(new FetchSourceContext(true, + new String[] { "included_field_1", "included_field_2" }, + new String[] { "excluded_field" })) // <4> .build(); // end::put-data-frame-analytics-source-config diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSourceTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSourceTests.java index d82e1999f3034..3fae44aad9060 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSourceTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSourceTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; @@ -35,9 +36,17 @@ public class DataFrameAnalyticsSourceTests extends AbstractXContentTestCase { public static DataFrameAnalyticsSource randomSourceConfig() { + FetchSourceContext sourceFiltering = null; + if (randomBoolean()) { + sourceFiltering = new FetchSourceContext(true, + generateRandomStringArray(10, 10, false, false), + generateRandomStringArray(10, 10, false, false)); + } + return DataFrameAnalyticsSource.builder() .setIndex(generateRandomStringArray(10, 10, false, false)) .setQueryConfig(randomBoolean() ? null : randomQueryConfig()) + .setSourceFiltering(sourceFiltering) .build(); } diff --git a/docs/java-rest/high-level/ml/put-data-frame-analytics.asciidoc b/docs/java-rest/high-level/ml/put-data-frame-analytics.asciidoc index c4e7184de7e04..91a97ad604cee 100644 --- a/docs/java-rest/high-level/ml/put-data-frame-analytics.asciidoc +++ b/docs/java-rest/high-level/ml/put-data-frame-analytics.asciidoc @@ -52,6 +52,7 @@ include-tagged::{doc-tests-file}[{api}-source-config] <1> Constructing a new DataFrameAnalyticsSource <2> The source index <3> The query from which to gather the data. If query is not set, a `match_all` query is used by default. +<4> Source filtering to select which fields will exist in the destination index. ===== QueryConfig diff --git a/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc b/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc index 62b5b121528a5..e8ee463c66af7 100644 --- a/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc +++ b/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc @@ -16,17 +16,18 @@ <>. `analyzed_fields`:: - (object) You can specify both `includes` and/or `excludes` patterns. If - `analyzed_fields` is not set, only the relevant fields will be included. For - example, all the numeric fields for {oldetection}. For the supported field - types, see <>. + (Optional, object) Specify `includes` and/or `excludes` patterns to select + which fields will be included in the analysis. If `analyzed_fields` is not set, + only the relevant fields will be included. For example, all the numeric fields + for {oldetection}. For the supported field types, see <>. + Also see the <> which helps understand field selection. `includes`::: - (array) An array of strings that defines the fields that will be included in + (Optional, array) An array of strings that defines the fields that will be included in the analysis. `excludes`::: - (array) An array of strings that defines the fields that will be excluded + (Optional, array) An array of strings that defines the fields that will be excluded from the analysis. @@ -81,8 +82,8 @@ PUT _ml/data_frame/analytics/loganalytics that setting. For more information, see <>. `source`:: - (object) The source configuration consisting an `index` and optionally a - `query` object. + (object) The configuration of how to source the analysis data. It requires an `index`. + Optionally, `query` and `_source` may be specified. `index`::: (Required, string or array) Index or indices on which to perform the @@ -96,6 +97,19 @@ PUT _ml/data_frame/analytics/loganalytics as this object is passed verbatim to {es}. By default, this property has the following value: `{"match_all": {}}`. + `_source`::: + (Optional, object) Specify `includes` and/or `excludes` patterns to select + which fields will be present in the destination. Fields that are excluded + cannot be included in the analysis. + + `includes`:::: + (array) An array of strings that defines the fields that will be included in + the destination. + + `excludes`:::: + (array) An array of strings that defines the fields that will be excluded + from the destination. + [[dfanalytics-types]] ==== Analysis objects @@ -277,4 +291,4 @@ improvement. If you override any parameters, then the optimization will calculate the value of the remaining parameters accordingly and use the value you provided for the overridden parameter. The number of rounds are reduced respectively. The validation error is estimated in each round by using 4-fold -cross validation. \ No newline at end of file +cross validation. diff --git a/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc index 159f0cb61a0c4..b4971fffa9c49 100644 --- a/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc @@ -101,13 +101,13 @@ single number. For example, in case of age ranges, you can model the values as <>. `analyzed_fields`:: - (Optional, object) You can specify both `includes` and/or `excludes` patterns. - If `analyzed_fields` is not set, only the relevant fields will be included. - For example, all the numeric fields for {oldetection}. For the supported field - types, see <>. If you specify fields – - either in `includes` or in `excludes` – that have a data type that is not - supported, an error occurs. - + (Optional, object) Specify `includes` and/or `excludes` patterns to select + which fields will be included in the analysis. If `analyzed_fields` is not set, + only the relevant fields will be included. For example, all the numeric fields + for {oldetection}. For the supported field types, see <>. + Also see the <> which helps understand + field selection. + `includes`::: (Optional, array) An array of strings that defines the fields that will be included in the analysis. @@ -142,20 +142,33 @@ single number. For example, in case of age ranges, you can model the values as that setting. For more information, see <>. `source`:: - (Required, object) The source configuration, consisting of `index` and - optionally a `query`. + (object) The configuration of how to source the analysis data. It requires an `index`. + Optionally, `query` and `_source` may be specified. - `index`::: - (Required, string or array) Index or indices on which to perform the - analysis. It can be a single index or index pattern as well as an array of - indices or patterns. - - `query`::: - (Optional, object) The {es} query domain-specific language - (<>). This value corresponds to the query object in an {es} - search POST body. All the options that are supported by {es} can be used, - as this object is passed verbatim to {es}. By default, this property has - the following value: `{"match_all": {}}`. + `index`::: + (Required, string or array) Index or indices on which to perform the + analysis. It can be a single index or index pattern as well as an array of + indices or patterns. + + `query`::: + (Optional, object) The {es} query domain-specific language + (<>). This value corresponds to the query object in an {es} + search POST body. All the options that are supported by {es} can be used, + as this object is passed verbatim to {es}. By default, this property has + the following value: `{"match_all": {}}`. + + `_source`::: + (Optional, object) Specify `includes` and/or `excludes` patterns to select + which fields will be present in the destination. Fields that are excluded + cannot be included in the analysis. + + `includes`:::: + (array) An array of strings that defines the fields that will be included in + the destination. + + `excludes`:::: + (array) An array of strings that defines the fields that will be excluded + from the destination. `allow_lazy_start`:: (Optional, boolean) Whether this job should be allowed to start when there diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PutDataFrameAnalyticsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PutDataFrameAnalyticsAction.java index 5bce41d8a4ae6..4f4ddc388aed7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PutDataFrameAnalyticsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PutDataFrameAnalyticsAction.java @@ -8,6 +8,7 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.ValidateActions; import org.elasticsearch.action.support.master.AcknowledgedRequest; import org.elasticsearch.action.support.master.MasterNodeOperationRequestBuilder; import org.elasticsearch.client.ElasticsearchClient; @@ -18,6 +19,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; +import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsSource; import org.elasticsearch.xpack.core.ml.job.messages.Messages; import java.io.IOException; @@ -87,6 +89,24 @@ public DataFrameAnalyticsConfig getConfig() { @Override public ActionRequestValidationException validate() { + ActionRequestValidationException error = null; + error = checkNoIncludedAnalyzedFieldsAreExcludedBySourceFiltering(config, error); + return error; + } + + private ActionRequestValidationException checkNoIncludedAnalyzedFieldsAreExcludedBySourceFiltering( + DataFrameAnalyticsConfig config, ActionRequestValidationException error) { + if (config.getAnalyzedFields() == null) { + return null; + } + for (String analyzedInclude : config.getAnalyzedFields().includes()) { + if (config.getSource().isFieldExcluded(analyzedInclude)) { + return ValidateActions.addValidationError("field [" + analyzedInclude + "] is included in [" + + DataFrameAnalyticsConfig.ANALYZED_FIELDS.getPreferredName() + "] but not in [" + + DataFrameAnalyticsConfig.SOURCE.getPreferredName() + "." + + DataFrameAnalyticsSource._SOURCE.getPreferredName() + "]", error); + } + } return null; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java index ac1589fa56fbc..9fd7f8aa86fcb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java @@ -127,7 +127,7 @@ private static DataFrameAnalysis parseAnalysis(XContentParser parser, boolean ig private final Version version; private final boolean allowLazyStart; - public DataFrameAnalyticsConfig(String id, String description, DataFrameAnalyticsSource source, DataFrameAnalyticsDest dest, + private DataFrameAnalyticsConfig(String id, String description, DataFrameAnalyticsSource source, DataFrameAnalyticsDest dest, DataFrameAnalysis analysis, Map headers, ByteSizeValue modelMemoryLimit, FetchSourceContext analyzedFields, Instant createTime, Version version, boolean allowLazyStart) { this.id = ExceptionsHelper.requireNonNull(id, ID); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSource.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSource.java index 5ffa3119413ab..c5e5515deb0b5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSource.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSource.java @@ -6,17 +6,21 @@ package org.elasticsearch.xpack.core.ml.dataframe; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.xpack.core.ml.job.messages.Messages; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import org.elasticsearch.xpack.core.ml.utils.QueryProvider; @@ -33,20 +37,29 @@ public class DataFrameAnalyticsSource implements Writeable, ToXContentObject { public static final ParseField INDEX = new ParseField("index"); public static final ParseField QUERY = new ParseField("query"); + public static final ParseField _SOURCE = new ParseField("_source"); public static ConstructingObjectParser createParser(boolean ignoreUnknownFields) { ConstructingObjectParser parser = new ConstructingObjectParser<>("data_frame_analytics_source", - ignoreUnknownFields, a -> new DataFrameAnalyticsSource(((List) a[0]).toArray(new String[0]), (QueryProvider) a[1])); + ignoreUnknownFields, a -> new DataFrameAnalyticsSource( + ((List) a[0]).toArray(new String[0]), + (QueryProvider) a[1], + (FetchSourceContext) a[2])); parser.declareStringArray(ConstructingObjectParser.constructorArg(), INDEX); parser.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> QueryProvider.fromXContent(p, ignoreUnknownFields, Messages.DATA_FRAME_ANALYTICS_BAD_QUERY_FORMAT), QUERY); + parser.declareField(ConstructingObjectParser.optionalConstructorArg(), + (p, c) -> FetchSourceContext.fromXContent(p), + _SOURCE, + ObjectParser.ValueType.OBJECT_ARRAY_BOOLEAN_OR_STRING); return parser; } private final String[] index; private final QueryProvider queryProvider; + private final FetchSourceContext sourceFiltering; - public DataFrameAnalyticsSource(String[] index, @Nullable QueryProvider queryProvider) { + public DataFrameAnalyticsSource(String[] index, @Nullable QueryProvider queryProvider, @Nullable FetchSourceContext sourceFiltering) { this.index = ExceptionsHelper.requireNonNull(index, INDEX); if (index.length == 0) { throw new IllegalArgumentException("source.index must specify at least one index"); @@ -55,22 +68,36 @@ public DataFrameAnalyticsSource(String[] index, @Nullable QueryProvider queryPro throw new IllegalArgumentException("source.index must contain non-null and non-empty strings"); } this.queryProvider = queryProvider == null ? QueryProvider.defaultQuery() : queryProvider; + if (sourceFiltering != null && sourceFiltering.fetchSource() == false) { + throw new IllegalArgumentException("source._source cannot be disabled"); + } + this.sourceFiltering = sourceFiltering; } public DataFrameAnalyticsSource(StreamInput in) throws IOException { index = in.readStringArray(); queryProvider = QueryProvider.fromStream(in); + if (in.getVersion().onOrAfter(Version.CURRENT)) { + sourceFiltering = in.readOptionalWriteable(FetchSourceContext::new); + } else { + sourceFiltering = null; + } } public DataFrameAnalyticsSource(DataFrameAnalyticsSource other) { this.index = Arrays.copyOf(other.index, other.index.length); this.queryProvider = new QueryProvider(other.queryProvider); + this.sourceFiltering = other.sourceFiltering == null ? null : new FetchSourceContext( + other.sourceFiltering.fetchSource(), other.sourceFiltering.includes(), other.sourceFiltering.excludes()); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeStringArray(index); queryProvider.writeTo(out); + if (out.getVersion().onOrAfter(Version.CURRENT)) { + out.writeOptionalWriteable(sourceFiltering); + } } @Override @@ -78,6 +105,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.startObject(); builder.array(INDEX.getPreferredName(), index); builder.field(QUERY.getPreferredName(), queryProvider.getQuery()); + if (sourceFiltering != null) { + builder.field(_SOURCE.getPreferredName(), sourceFiltering); + } builder.endObject(); return builder; } @@ -89,12 +119,13 @@ public boolean equals(Object o) { DataFrameAnalyticsSource other = (DataFrameAnalyticsSource) o; return Arrays.equals(index, other.index) - && Objects.equals(queryProvider, other.queryProvider); + && Objects.equals(queryProvider, other.queryProvider) + && Objects.equals(sourceFiltering, other.sourceFiltering); } @Override public int hashCode() { - return Objects.hash(Arrays.asList(index), queryProvider); + return Objects.hash(Arrays.asList(index), queryProvider, sourceFiltering); } public String[] getIndex() { @@ -118,6 +149,10 @@ public QueryBuilder getParsedQuery() { return queryProvider.getParsedQuery(); } + public FetchSourceContext getSourceFiltering() { + return sourceFiltering; + } + Exception getQueryParsingException() { return queryProvider.getParsingException(); } @@ -147,4 +182,47 @@ public List getQueryDeprecations(NamedXContentRegistry namedXContentRegi Map getQuery() { return queryProvider.getQuery(); } + + public boolean isFieldExcluded(String path) { + if (sourceFiltering == null) { + return false; + } + + // First we check in the excludes as they are applied last + for (String exclude : sourceFiltering.excludes()) { + if (pathMatchesSourcePattern(path, exclude)) { + return true; + } + } + + // Now we can check the includes + + // Empty includes means no further exclusions + if (sourceFiltering.includes().length == 0) { + return false; + } + + for (String include : sourceFiltering.includes()) { + if (pathMatchesSourcePattern(path, include)) { + return false; + } + } + return true; + } + + private static boolean pathMatchesSourcePattern(String path, String sourcePattern) { + if (sourcePattern.equals(path)) { + return true; + } + + if (Regex.isSimpleMatchPattern(sourcePattern)) { + return Regex.simpleMatch(sourcePattern, path); + } + + // At this stage sourcePattern is a concrete field name and path is not equal to it. + // We should check if path is a nested field of pattern. + // Let us take "foo" as an example. + // Fields that are "foo.*" should also be matched. + return Regex.simpleMatch(sourcePattern + ".*", path); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappings.java index 95d8194397a54..b9de87ef93de0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappings.java @@ -418,6 +418,9 @@ public static void addDataFrameAnalyticsFields(XContentBuilder builder) throws I .startObject(DataFrameAnalyticsSource.QUERY.getPreferredName()) .field(ENABLED, false) .endObject() + .startObject(DataFrameAnalyticsSource._SOURCE.getPreferredName()) + .field(ENABLED, false) + .endObject() .endObject() .endObject() .startObject(DataFrameAnalyticsConfig.DEST.getPreferredName()) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ReservedFieldNames.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ReservedFieldNames.java index e0ae183d17a0f..8eacdcb0e78e4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ReservedFieldNames.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ReservedFieldNames.java @@ -303,6 +303,7 @@ public final class ReservedFieldNames { DataFrameAnalyticsDest.RESULTS_FIELD.getPreferredName(), DataFrameAnalyticsSource.INDEX.getPreferredName(), DataFrameAnalyticsSource.QUERY.getPreferredName(), + DataFrameAnalyticsSource._SOURCE.getPreferredName(), OutlierDetection.NAME.getPreferredName(), OutlierDetection.N_NEIGHBORS.getPreferredName(), OutlierDetection.METHOD.getPreferredName(), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PutDataFrameAnalyticsActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PutDataFrameAnalyticsActionRequestTests.java index dbd3db927503c..9d194bc260523 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PutDataFrameAnalyticsActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PutDataFrameAnalyticsActionRequestTests.java @@ -11,16 +11,25 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.action.PutDataFrameAnalyticsAction.Request; +import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfigTests; +import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsSource; import org.elasticsearch.xpack.core.ml.dataframe.analyses.MlDataFrameAnalysisNamedXContentProvider; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.OutlierDetectionTests; import org.junit.Before; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + public class PutDataFrameAnalyticsActionRequestTests extends AbstractSerializingTestCase { private String id; @@ -65,4 +74,39 @@ protected boolean supportsUnknownFields() { protected Request doParseInstance(XContentParser parser) { return Request.parseRequest(id, parser); } + + public void testValidate_GivenRequestWithIncludedAnalyzedFieldThatIsExcludedInSourceFiltering() { + DataFrameAnalyticsSource source = new DataFrameAnalyticsSource(new String[] {"index"}, null, + new FetchSourceContext(true, null, new String[] {"excluded"})); + FetchSourceContext analyzedFields = new FetchSourceContext(true, new String[] {"excluded"}, null); + DataFrameAnalyticsConfig config = new DataFrameAnalyticsConfig.Builder() + .setId("foo") + .setSource(source) + .setAnalysis(OutlierDetectionTests.createRandom()) + .setAnalyzedFields(analyzedFields) + .buildForExplain(); + Request request = new Request(config); + + Exception e = request.validate(); + + assertThat(e, is(notNullValue())); + assertThat(e.getMessage(), containsString("field [excluded] is included in [analyzed_fields] but not in [source._source]")); + } + + public void testValidate_GivenRequestWithIncludedAnalyzedFieldThatIsIncludedInSourceFiltering() { + DataFrameAnalyticsSource source = new DataFrameAnalyticsSource(new String[] {"index"}, null, + new FetchSourceContext(true, new String[] {"included"}, null)); + FetchSourceContext analyzedFields = new FetchSourceContext(true, new String[] {"included"}, null); + DataFrameAnalyticsConfig config = new DataFrameAnalyticsConfig.Builder() + .setId("foo") + .setSource(source) + .setAnalysis(OutlierDetectionTests.createRandom()) + .setAnalyzedFields(analyzedFields) + .buildForExplain(); + Request request = new Request(config); + + Exception e = request.validate(); + + assertThat(e, is(nullValue())); + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSourceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSourceTests.java index 36c4774baa465..cd58f9c84533c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSourceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSourceTests.java @@ -12,12 +12,18 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.utils.QueryProvider; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.Arrays; import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; public class DataFrameAnalyticsSourceTests extends AbstractSerializingTestCase { @@ -46,6 +52,7 @@ protected DataFrameAnalyticsSource createTestInstance() { public static DataFrameAnalyticsSource createRandom() { String[] index = generateRandomStringArray(10, 10, false, false); QueryProvider queryProvider = null; + FetchSourceContext sourceFiltering = null; if (randomBoolean()) { try { queryProvider = QueryProvider.fromParsedQuery(QueryBuilders.termQuery(randomAlphaOfLength(10), randomAlphaOfLength(10))); @@ -54,11 +61,75 @@ public static DataFrameAnalyticsSource createRandom() { throw new UncheckedIOException(e); } } - return new DataFrameAnalyticsSource(index, queryProvider); + if (randomBoolean()) { + sourceFiltering = new FetchSourceContext(true, + generateRandomStringArray(10, 10, false, false), + generateRandomStringArray(10, 10, false, false)); + } + return new DataFrameAnalyticsSource(index, queryProvider, sourceFiltering); } @Override protected Writeable.Reader instanceReader() { return DataFrameAnalyticsSource::new; } + + public void testConstructor_GivenDisabledSource() { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new DataFrameAnalyticsSource( + new String[] {"index"}, null, new FetchSourceContext(false, null, null))); + assertThat(e.getMessage(), equalTo("source._source cannot be disabled")); + } + + public void testIsFieldExcluded_GivenNoSourceFiltering() { + DataFrameAnalyticsSource source = new DataFrameAnalyticsSource(new String[] { "index" }, null, null); + assertThat(source.isFieldExcluded(randomAlphaOfLength(10)), is(false)); + } + + public void testIsFieldExcluded_GivenSourceFilteringWithNulls() { + DataFrameAnalyticsSource source = new DataFrameAnalyticsSource(new String[] { "index" }, null, + new FetchSourceContext(true, null, null)); + assertThat(source.isFieldExcluded(randomAlphaOfLength(10)), is(false)); + } + + public void testIsFieldExcluded_GivenExcludes() { + assertThat(newSourceWithExcludes("foo").isFieldExcluded("bar"), is(false)); + assertThat(newSourceWithExcludes("foo").isFieldExcluded("foo"), is(true)); + assertThat(newSourceWithExcludes("foo").isFieldExcluded("foo.bar"), is(true)); + assertThat(newSourceWithExcludes("foo*").isFieldExcluded("foo"), is(true)); + assertThat(newSourceWithExcludes("foo*").isFieldExcluded("foobar"), is(true)); + assertThat(newSourceWithExcludes("foo*").isFieldExcluded("foo.bar"), is(true)); + assertThat(newSourceWithExcludes("foo*").isFieldExcluded("foo*"), is(true)); + assertThat(newSourceWithExcludes("foo*").isFieldExcluded("fo*"), is(false)); + } + + public void testIsFieldExcluded_GivenIncludes() { + assertThat(newSourceWithIncludes("foo").isFieldExcluded("bar"), is(true)); + assertThat(newSourceWithIncludes("foo").isFieldExcluded("foo"), is(false)); + assertThat(newSourceWithIncludes("foo").isFieldExcluded("foo.bar"), is(false)); + assertThat(newSourceWithIncludes("foo*").isFieldExcluded("foo"), is(false)); + assertThat(newSourceWithIncludes("foo*").isFieldExcluded("foobar"), is(false)); + assertThat(newSourceWithIncludes("foo*").isFieldExcluded("foo.bar"), is(false)); + assertThat(newSourceWithIncludes("foo*").isFieldExcluded("foo*"), is(false)); + assertThat(newSourceWithIncludes("foo*").isFieldExcluded("fo*"), is(true)); + } + + public void testIsFieldExcluded_GivenIncludesAndExcludes() { + // Excludes take precedence + assertThat(newSourceWithIncludesExcludes(Collections.singletonList("foo"), Collections.singletonList("foo")) + .isFieldExcluded("foo"), is(true)); + } + + private static DataFrameAnalyticsSource newSourceWithIncludes(String... includes) { + return newSourceWithIncludesExcludes(Arrays.asList(includes), Collections.emptyList()); + } + + private static DataFrameAnalyticsSource newSourceWithExcludes(String... excludes) { + return newSourceWithIncludesExcludes(Collections.emptyList(), Arrays.asList(excludes)); + } + + private static DataFrameAnalyticsSource newSourceWithIncludesExcludes(List includes, List excludes) { + FetchSourceContext sourceFiltering = new FetchSourceContext(true, + includes.toArray(new String[0]), excludes.toArray(new String[0])); + return new DataFrameAnalyticsSource(new String[] { "index" } , null, sourceFiltering); + } } diff --git a/x-pack/plugin/ml/qa/ml-with-security/build.gradle b/x-pack/plugin/ml/qa/ml-with-security/build.gradle index 38beb1d1908c1..fe53f1ca39ff5 100644 --- a/x-pack/plugin/ml/qa/ml-with-security/build.gradle +++ b/x-pack/plugin/ml/qa/ml-with-security/build.gradle @@ -52,6 +52,7 @@ integTest.runner { 'ml/data_frame_analytics_crud/Test put config with dest index included in source via alias', 'ml/data_frame_analytics_crud/Test put config with unknown top level field', 'ml/data_frame_analytics_crud/Test put config with unknown field in outlier detection analysis', + 'ml/data_frame_analytics_crud/Test put config given analyzed_fields include field excluded by source', 'ml/data_frame_analytics_crud/Test put config given missing source', 'ml/data_frame_analytics_crud/Test put config given source with empty index array', 'ml/data_frame_analytics_crud/Test put config given source with empty string in index array', diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java index 6796e3b7223d7..540d9f373b7e4 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java @@ -53,7 +53,8 @@ public void testSourceQueryIsApplied() throws IOException { DataFrameAnalyticsConfig config = new DataFrameAnalyticsConfig.Builder() .setId(id) .setSource(new DataFrameAnalyticsSource(new String[] { sourceIndex }, - QueryProvider.fromParsedQuery(QueryBuilders.termQuery("categorical", "only-one")))) + QueryProvider.fromParsedQuery(QueryBuilders.termQuery("categorical", "only-one")), + null)) .setAnalysis(new Classification("categorical")) .buildForExplain(); diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java index 06c88a9793b23..b3b58a2d4fcbb 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java @@ -164,7 +164,7 @@ protected static DataFrameAnalyticsConfig buildAnalytics(String id, String sourc @Nullable String resultsField, DataFrameAnalysis analysis) { return new DataFrameAnalyticsConfig.Builder() .setId(id) - .setSource(new DataFrameAnalyticsSource(new String[] { sourceIndex }, null)) + .setSource(new DataFrameAnalyticsSource(new String[] { sourceIndex }, null, null)) .setDest(new DataFrameAnalyticsDest(destIndex, resultsField)) .setAnalysis(analysis) .build(); diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RunDataFrameAnalyticsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RunDataFrameAnalyticsIT.java index c30fccce2f17a..2628a751bc112 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RunDataFrameAnalyticsIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RunDataFrameAnalyticsIT.java @@ -356,7 +356,7 @@ public void testOutlierDetectionWithMultipleSourceIndices() throws Exception { String id = "test_outlier_detection_with_multiple_source_indices"; DataFrameAnalyticsConfig config = new DataFrameAnalyticsConfig.Builder() .setId(id) - .setSource(new DataFrameAnalyticsSource(sourceIndex, null)) + .setSource(new DataFrameAnalyticsSource(sourceIndex, null, null)) .setDest(new DataFrameAnalyticsDest(destIndex, null)) .setAnalysis(new OutlierDetection.Builder().build()) .build(); @@ -472,7 +472,7 @@ public void testModelMemoryLimitLowerThanEstimatedMemoryUsage() throws Exception ByteSizeValue modelMemoryLimit = new ByteSizeValue(1, ByteSizeUnit.MB); DataFrameAnalyticsConfig config = new DataFrameAnalyticsConfig.Builder() .setId(id) - .setSource(new DataFrameAnalyticsSource(new String[] { sourceIndex }, null)) + .setSource(new DataFrameAnalyticsSource(new String[] { sourceIndex }, null, null)) .setDest(new DataFrameAnalyticsDest(sourceIndex + "-results", null)) .setAnalysis(new OutlierDetection.Builder().build()) .setModelMemoryLimit(modelMemoryLimit) @@ -516,7 +516,7 @@ public void testLazyAssignmentWithModelMemoryLimitTooHighForAssignment() throws ByteSizeValue modelMemoryLimit = new ByteSizeValue(1, ByteSizeUnit.TB); DataFrameAnalyticsConfig config = new DataFrameAnalyticsConfig.Builder() .setId(id) - .setSource(new DataFrameAnalyticsSource(new String[] { sourceIndex }, null)) + .setSource(new DataFrameAnalyticsSource(new String[] { sourceIndex }, null, null)) .setDest(new DataFrameAnalyticsDest(sourceIndex + "-results", null)) .setAnalysis(new OutlierDetection.Builder().build()) .setModelMemoryLimit(modelMemoryLimit) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java index f76014eda1910..65ed0b93aa7b9 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java @@ -237,7 +237,7 @@ private void getStartContext(String id, ActionListener finalListen // Step 5. Validate mappings can be merged ActionListener toValidateMappingsListener = ActionListener.wrap( startContext -> MappingsMerger.mergeMappings(client, startContext.config.getHeaders(), - startContext.config.getSource().getIndex(), ActionListener.wrap( + startContext.config.getSource(), ActionListener.wrap( mappings -> validateMappingsMergeListener.onResponse(startContext), finalListener::onFailure)), finalListener::onFailure ); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndex.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndex.java index 444b0081cc7bc..a369bc7d0b09a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndex.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndex.java @@ -84,8 +84,6 @@ private static void prepareCreateIndexRequest(Client client, Clock clock, DataFr ActionListener listener) { AtomicReference settingsHolder = new AtomicReference<>(); - String[] sourceIndex = config.getSource().getIndex(); - ActionListener> mappingsListener = ActionListener.wrap( mappings -> listener.onResponse(createIndexRequest(clock, config, settingsHolder.get(), mappings)), listener::onFailure @@ -94,7 +92,7 @@ private static void prepareCreateIndexRequest(Client client, Clock clock, DataFr ActionListener settingsListener = ActionListener.wrap( settings -> { settingsHolder.set(settings); - MappingsMerger.mergeMappings(client, config.getHeaders(), sourceIndex, mappingsListener); + MappingsMerger.mergeMappings(client, config.getHeaders(), config.getSource(), mappingsListener); }, listener::onFailure ); @@ -105,7 +103,7 @@ private static void prepareCreateIndexRequest(Client client, Clock clock, DataFr ); GetSettingsRequest getSettingsRequest = new GetSettingsRequest(); - getSettingsRequest.indices(sourceIndex); + getSettingsRequest.indices(config.getSource().getIndex()); getSettingsRequest.indicesOptions(IndicesOptions.lenientExpandOpen()); getSettingsRequest.names(PRESERVED_SETTINGS); ClientHelper.executeWithHeadersAsync(config.getHeaders(), ML_ORIGIN, client, GetSettingsAction.INSTANCE, diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java index c53238dc425d7..76fc588027943 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java @@ -179,6 +179,7 @@ private void reindexDataframeAndStartAnalysis(DataFrameAnalyticsTask task, DataF ReindexRequest reindexRequest = new ReindexRequest(); reindexRequest.setSourceIndices(config.getSource().getIndex()); reindexRequest.setSourceQuery(config.getSource().getParsedQuery()); + reindexRequest.getSearchRequest().source().fetchSource(config.getSource().getSourceFiltering()); reindexRequest.setDestIndex(config.getDest().getIndex()); reindexRequest.setScript(new Script("ctx._source." + DataFrameAnalyticsIndex.ID_COPY + " = ctx._id")); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/MappingsMerger.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/MappingsMerger.java index c573f193cf01a..056f8239aef8d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/MappingsMerger.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/MappingsMerger.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.xpack.core.ClientHelper; +import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsSource; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.util.Collections; @@ -32,22 +33,22 @@ public final class MappingsMerger { private MappingsMerger() {} - public static void mergeMappings(Client client, Map headers, String[] index, + public static void mergeMappings(Client client, Map headers, DataFrameAnalyticsSource source, ActionListener> listener) { ActionListener mappingsListener = ActionListener.wrap( - getMappingsResponse -> listener.onResponse(MappingsMerger.mergeMappings(getMappingsResponse)), + getMappingsResponse -> listener.onResponse(MappingsMerger.mergeMappings(source, getMappingsResponse)), listener::onFailure ); GetMappingsRequest getMappingsRequest = new GetMappingsRequest(); - getMappingsRequest.indices(index); + getMappingsRequest.indices(source.getIndex()); ClientHelper.executeWithHeadersAsync(headers, ML_ORIGIN, client, GetMappingsAction.INSTANCE, getMappingsRequest, mappingsListener); } - static ImmutableOpenMap mergeMappings(GetMappingsResponse getMappingsResponse) { + static ImmutableOpenMap mergeMappings(DataFrameAnalyticsSource source, + GetMappingsResponse getMappingsResponse) { ImmutableOpenMap indexToMappings = getMappingsResponse.getMappings(); - String type = null; Map mergedMappings = new HashMap<>(); Iterator> iterator = indexToMappings.iterator(); @@ -61,13 +62,16 @@ static ImmutableOpenMap mergeMappings(GetMappingsRespon Map fieldMappings = (Map) currentMappings.get("properties"); for (Map.Entry fieldMapping : fieldMappings.entrySet()) { - if (mergedMappings.containsKey(fieldMapping.getKey())) { - if (mergedMappings.get(fieldMapping.getKey()).equals(fieldMapping.getValue()) == false) { - throw ExceptionsHelper.badRequestException("cannot merge mappings because of differences for field [{}]", - fieldMapping.getKey()); + String field = fieldMapping.getKey(); + if (source.isFieldExcluded(field) == false) { + if (mergedMappings.containsKey(field)) { + if (mergedMappings.get(field).equals(fieldMapping.getValue()) == false) { + throw ExceptionsHelper.badRequestException( + "cannot merge mappings because of differences for field [{}]", field); + } + } else { + mergedMappings.put(field, fieldMapping.getValue()); } - } else { - mergedMappings.put(fieldMapping.getKey(), fieldMapping.getValue()); } } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java index 42ea2d1944301..62184e290374d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java @@ -85,6 +85,7 @@ private Set getIncludedFields(Set fieldSelection) { fields.removeAll(IGNORE_FIELDS); checkResultsFieldIsNotPresent(); removeFieldsUnderResultsField(fields); + applySourceFiltering(fields); FetchSourceContext analyzedFields = config.getAnalyzedFields(); // If the user has not explicitly included fields we'll include all compatible fields @@ -132,6 +133,16 @@ private void checkResultsFieldIsNotPresent() { } } + private void applySourceFiltering(Set fields) { + Iterator fieldsIterator = fields.iterator(); + while (fieldsIterator.hasNext()) { + String field = fieldsIterator.next(); + if (config.getSource().isFieldExcluded(field)) { + fieldsIterator.remove(); + } + } + } + private void addExcludedField(String field, String reason, Set fieldSelection) { fieldSelection.add(FieldSelection.excluded(field, getMappingTypes(field), reason)); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndexTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndexTests.java index 063e1ea337782..950d5997a5a35 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndexTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndexTests.java @@ -58,7 +58,7 @@ public class DataFrameAnalyticsIndexTests extends ESTestCase { private static final DataFrameAnalyticsConfig ANALYTICS_CONFIG = new DataFrameAnalyticsConfig.Builder() .setId(ANALYTICS_ID) - .setSource(new DataFrameAnalyticsSource(SOURCE_INDEX, null)) + .setSource(new DataFrameAnalyticsSource(SOURCE_INDEX, null, null)) .setDest(new DataFrameAnalyticsDest(DEST_INDEX, null)) .setAnalysis(new OutlierDetection.Builder().build()) .build(); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/MappingsMergerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/MappingsMergerTests.java index f44e8a9f3e61a..e47b3e6934e80 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/MappingsMergerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/MappingsMergerTests.java @@ -10,9 +10,10 @@ import org.elasticsearch.cluster.metadata.MappingMetaData; import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsSource; -import java.io.IOException; import java.util.Map; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -21,7 +22,7 @@ public class MappingsMergerTests extends ESTestCase { - public void testMergeMappings_GivenIndicesWithIdenticalMappings() throws IOException { + public void testMergeMappings_GivenIndicesWithIdenticalMappings() { Map index1Mappings = Map.of("properties", Map.of("field_1", "field_1_mappings", "field_2", "field_2_mappings")); MappingMetaData index1MappingMetaData = new MappingMetaData("_doc", index1Mappings); @@ -34,14 +35,14 @@ public void testMergeMappings_GivenIndicesWithIdenticalMappings() throws IOExcep GetMappingsResponse getMappingsResponse = new GetMappingsResponse(mappings.build()); - ImmutableOpenMap mergedMappings = MappingsMerger.mergeMappings(getMappingsResponse); + ImmutableOpenMap mergedMappings = MappingsMerger.mergeMappings(newSource(), getMappingsResponse); assertThat(mergedMappings.size(), equalTo(1)); assertThat(mergedMappings.containsKey("_doc"), is(true)); assertThat(mergedMappings.valuesIt().next().getSourceAsMap(), equalTo(index1Mappings)); } - public void testMergeMappings_GivenFieldWithDifferentMapping() throws IOException { + public void testMergeMappings_GivenFieldWithDifferentMapping() { Map index1Mappings = Map.of("properties", Map.of("field_1", "field_1_mappings")); MappingMetaData index1MappingMetaData = new MappingMetaData("_doc", index1Mappings); @@ -55,12 +56,12 @@ public void testMergeMappings_GivenFieldWithDifferentMapping() throws IOExceptio GetMappingsResponse getMappingsResponse = new GetMappingsResponse(mappings.build()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> MappingsMerger.mergeMappings(getMappingsResponse)); + () -> MappingsMerger.mergeMappings(newSource(), getMappingsResponse)); assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); assertThat(e.getMessage(), equalTo("cannot merge mappings because of differences for field [field_1]")); } - public void testMergeMappings_GivenIndicesWithDifferentMappingsButNoConflicts() throws IOException { + public void testMergeMappings_GivenIndicesWithDifferentMappingsButNoConflicts() { Map index1Mappings = Map.of("properties", Map.of("field_1", "field_1_mappings", "field_2", "field_2_mappings")); MappingMetaData index1MappingMetaData = new MappingMetaData("_doc", index1Mappings); @@ -75,7 +76,7 @@ public void testMergeMappings_GivenIndicesWithDifferentMappingsButNoConflicts() GetMappingsResponse getMappingsResponse = new GetMappingsResponse(mappings.build()); - ImmutableOpenMap mergedMappings = MappingsMerger.mergeMappings(getMappingsResponse); + ImmutableOpenMap mergedMappings = MappingsMerger.mergeMappings(newSource(), getMappingsResponse); assertThat(mergedMappings.size(), equalTo(1)); assertThat(mergedMappings.containsKey("_doc"), is(true)); @@ -92,4 +93,35 @@ public void testMergeMappings_GivenIndicesWithDifferentMappingsButNoConflicts() assertThat(fieldMappings.get("field_2"), equalTo("field_2_mappings")); assertThat(fieldMappings.get("field_3"), equalTo("field_3_mappings")); } + + public void testMergeMappings_GivenSourceFiltering() { + Map indexMappings = Map.of("properties", Map.of("field_1", "field_1_mappings", "field_2", "field_2_mappings")); + MappingMetaData indexMappingMetaData = new MappingMetaData("_doc", indexMappings); + + ImmutableOpenMap.Builder mappings = ImmutableOpenMap.builder(); + mappings.put("index", indexMappingMetaData); + + GetMappingsResponse getMappingsResponse = new GetMappingsResponse(mappings.build()); + + ImmutableOpenMap mergedMappings = MappingsMerger.mergeMappings( + newSourceWithExcludes("field_1"), getMappingsResponse); + + assertThat(mergedMappings.size(), equalTo(1)); + assertThat(mergedMappings.containsKey("_doc"), is(true)); + Map mappingsAsMap = mergedMappings.valuesIt().next().getSourceAsMap(); + @SuppressWarnings("unchecked") + Map fieldMappings = (Map) mappingsAsMap.get("properties"); + + assertThat(fieldMappings.size(), equalTo(1)); + assertThat(fieldMappings.containsKey("field_2"), is(true)); + } + + private static DataFrameAnalyticsSource newSource() { + return new DataFrameAnalyticsSource(new String[] {"index"}, null, null); + } + + private static DataFrameAnalyticsSource newSourceWithExcludes(String... excludes) { + return new DataFrameAnalyticsSource(new String[] {"index"}, null, + new FetchSourceContext(true, null, excludes)); + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/SourceDestValidatorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/SourceDestValidatorTests.java index c9423aadbe03a..ec8b97942ac0a 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/SourceDestValidatorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/SourceDestValidatorTests.java @@ -183,6 +183,6 @@ public void testCheck_GivenDestIndexIsAliasThatIsIncludedInSource() { } private static DataFrameAnalyticsSource createSource(String... index) { - return new DataFrameAnalyticsSource(index, null); + return new DataFrameAnalyticsSource(index, null, null); } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java index 25553627a9e05..f4f25bcfa0636 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java @@ -45,6 +45,9 @@ public class ExtractedFieldsDetectorTests extends ESTestCase { private static final String DEST_INDEX = "dest_index"; private static final String RESULTS_FIELD = "ml"; + private FetchSourceContext sourceFiltering; + private FetchSourceContext analyzedFields; + public void testDetect_GivenFloatField() { FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() .addAggregatableField("some_float", "float").build(); @@ -86,8 +89,8 @@ public void testDetect_GivenOutlierDetectionAndNonNumericField() { .addAggregatableField("some_keyword", "keyword").build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No compatible fields could be detected in index [source_index]." + " Supported types are [boolean, byte, double, float, half_float, integer, long, scaled_float, short].")); @@ -99,7 +102,7 @@ public void testDetect_GivenOutlierDetectionAndFieldWithNumericAndNonNumericType ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No compatible fields could be detected in index [source_index]. " + "Supported types are [boolean, byte, double, float, half_float, integer, long, scaled_float, short].")); @@ -171,7 +174,7 @@ public void testDetect_GivenRegressionAndRequiredFieldMissing() { ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( SOURCE_INDEX, buildRegressionConfig("foo"), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("required field [foo] is missing; analysis requires fields [foo]")); } @@ -183,11 +186,11 @@ public void testDetect_GivenRegressionAndRequiredFieldExcluded() { .addAggregatableField("some_keyword", "keyword") .addAggregatableField("foo", "float") .build(); - FetchSourceContext analyzedFields = new FetchSourceContext(true, new String[0], new String[] {"foo"}); + analyzedFields = new FetchSourceContext(true, new String[0], new String[] {"foo"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("foo", analyzedFields), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + SOURCE_INDEX, buildRegressionConfig("foo"), false, 100, fieldCapabilities, Collections.emptyMap()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("required field [foo] is missing; analysis requires fields [foo]")); } @@ -199,11 +202,11 @@ public void testDetect_GivenRegressionAndRequiredFieldNotIncluded() { .addAggregatableField("some_keyword", "keyword") .addAggregatableField("foo", "float") .build(); - FetchSourceContext analyzedFields = new FetchSourceContext(true, new String[] {"some_float", "some_keyword"}, new String[0]); + analyzedFields = new FetchSourceContext(true, new String[] {"some_float", "some_keyword"}, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("foo", analyzedFields), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + SOURCE_INDEX, buildRegressionConfig("foo"), false, 100, fieldCapabilities, Collections.emptyMap()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("required field [foo] is missing; analysis requires fields [foo]")); } @@ -213,10 +216,10 @@ public void testDetect_GivenFieldIsBothIncludedAndExcluded() { .addAggregatableField("foo", "float") .addAggregatableField("bar", "float") .build(); - FetchSourceContext analyzedFields = new FetchSourceContext(true, new String[] {"foo", "bar"}, new String[] {"foo"}); + analyzedFields = new FetchSourceContext(true, new String[] {"foo", "bar"}, new String[] {"foo"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(analyzedFields), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); @@ -239,7 +242,7 @@ public void testDetect_GivenRegressionAndRequiredFieldHasInvalidType() { ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( SOURCE_INDEX, buildRegressionConfig("foo"), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("invalid types [keyword] for required field [foo]; " + "expected types are [byte, double, float, half_float, integer, long, scaled_float, short]")); @@ -255,7 +258,7 @@ public void testDetect_GivenClassificationAndRequiredFieldHasInvalidType() { ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( SOURCE_INDEX, buildClassificationConfig("some_float"), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("invalid types [float] for required field [some_float]; " + "expected types are [boolean, byte, integer, ip, keyword, long, short, text]")); @@ -270,7 +273,7 @@ public void testDetect_GivenClassificationAndDependentVariableHasInvalidCardinal ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector(SOURCE_INDEX, buildClassificationConfig("some_keyword"), false, 100, fieldCapabilities, Collections.singletonMap("some_keyword", 3L)); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("Field [some_keyword] must have at most [2] distinct values but there were at least [3]")); } @@ -281,7 +284,7 @@ public void testDetect_GivenIgnoredField() { ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No compatible fields could be detected in index [source_index]. " + "Supported types are [boolean, byte, double, float, half_float, integer, long, scaled_float, short].")); @@ -291,11 +294,11 @@ public void testDetect_GivenIncludedIgnoredField() { FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() .addAggregatableField("_id", "float") .build(); - FetchSourceContext analyzedFields = new FetchSourceContext(true, new String[]{"_id"}, new String[0]); + analyzedFields = new FetchSourceContext(true, new String[]{"_id"}, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(analyzedFields), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No field [_id] could be detected")); } @@ -304,11 +307,11 @@ public void testDetect_GivenExcludedFieldIsMissing() { FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() .addAggregatableField("foo", "float") .build(); - FetchSourceContext analyzedFields = new FetchSourceContext(true, new String[]{"*"}, new String[] {"bar"}); + analyzedFields = new FetchSourceContext(true, new String[]{"*"}, new String[] {"bar"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(analyzedFields), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No field [bar] could be detected")); } @@ -318,10 +321,10 @@ public void testDetect_GivenExcludedFieldIsUnsupported() { .addAggregatableField("numeric", "float") .addAggregatableField("categorical", "keyword") .build(); - FetchSourceContext analyzedFields = new FetchSourceContext(true, null, new String[] {"categorical"}); + analyzedFields = new FetchSourceContext(true, null, new String[] {"categorical"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(analyzedFields), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); @@ -366,11 +369,11 @@ public void testDetect_GivenIncludeWithMissingField() { .addAggregatableField("my_field2", "float") .build(); - FetchSourceContext desiredFields = new FetchSourceContext(true, new String[]{"your_field1", "my*"}, new String[0]); + analyzedFields = new FetchSourceContext(true, new String[]{"your_field1", "my*"}, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(desiredFields), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No field [your_field1] could be detected")); } @@ -381,11 +384,11 @@ public void testDetect_GivenExcludeAllValidFields() { .addAggregatableField("my_field2", "float") .build(); - FetchSourceContext desiredFields = new FetchSourceContext(true, new String[0], new String[]{"my_*"}); + analyzedFields = new FetchSourceContext(true, new String[0], new String[]{"my_*"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(desiredFields), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No compatible fields could be detected in index [source_index]. " + "Supported types are [boolean, byte, double, float, half_float, integer, long, scaled_float, short].")); } @@ -397,10 +400,10 @@ public void testDetect_GivenInclusionsAndExclusions() { .addAggregatableField("your_field2", "float") .build(); - FetchSourceContext desiredFields = new FetchSourceContext(true, new String[]{"your*", "my_*"}, new String[]{"*nope"}); + analyzedFields = new FetchSourceContext(true, new String[]{"your*", "my_*"}, new String[]{"*nope"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(desiredFields), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) @@ -422,11 +425,11 @@ public void testDetect_GivenIncludedFieldHasUnsupportedType() { .addAggregatableField("your_keyword", "keyword") .build(); - FetchSourceContext desiredFields = new FetchSourceContext(true, new String[]{"your*", "my_*"}, new String[]{"*nope"}); + analyzedFields = new FetchSourceContext(true, new String[]{"your*", "my_*"}, new String[]{"*nope"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(desiredFields), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("field [your_keyword] has unsupported type [keyword]. " + "Supported types are [boolean, byte, double, float, half_float, integer, long, scaled_float, short].")); @@ -442,7 +445,7 @@ public void testDetect_GivenIndexContainsResultsField() { ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("A field that matches the dest.results_field [ml] already exists; " + "please set a different results_field")); @@ -479,11 +482,11 @@ public void testDetect_GivenIncludedResultsField() { .addAggregatableField("your_field2", "float") .addAggregatableField("your_keyword", "keyword") .build(); - FetchSourceContext analyzedFields = new FetchSourceContext(true, new String[]{RESULTS_FIELD}, new String[0]); + analyzedFields = new FetchSourceContext(true, new String[]{RESULTS_FIELD}, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(analyzedFields), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("A field that matches the dest.results_field [ml] already exists; " + "please set a different results_field")); @@ -496,11 +499,11 @@ public void testDetect_GivenIncludedResultsFieldAndTaskIsRestarting() { .addAggregatableField("your_field2", "float") .addAggregatableField("your_keyword", "keyword") .build(); - FetchSourceContext analyzedFields = new FetchSourceContext(true, new String[]{RESULTS_FIELD}, new String[0]); + analyzedFields = new FetchSourceContext(true, new String[]{RESULTS_FIELD}, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(analyzedFields), true, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> extractedFieldsDetector.detect()); + SOURCE_INDEX, buildOutlierDetectionConfig(), true, 100, fieldCapabilities, Collections.emptyMap()); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No field [ml] could be detected")); } @@ -814,10 +817,10 @@ public void testDetect_GivenMultiFields_AndExplicitlyIncludedFields() { .addAggregatableField("field_1.keyword", "keyword") .addAggregatableField("field_2", "float") .build(); - FetchSourceContext analyzedFields = new FetchSourceContext(true, new String[] { "field_1", "field_2" }, new String[0]); + analyzedFields = new FetchSourceContext(true, new String[] { "field_1", "field_2" }, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("field_2", analyzedFields), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("field_2"), false, 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); assertThat(fieldExtraction.v1().getAllFields().size(), equalTo(2)); @@ -832,38 +835,76 @@ public void testDetect_GivenMultiFields_AndExplicitlyIncludedFields() { ); } - private static DataFrameAnalyticsConfig buildOutlierDetectionConfig() { - return buildOutlierDetectionConfig(null); + public void testDetect_GivenSourceFilteringWithIncludes() { + FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() + .addAggregatableField("field_11", "float") + .addAggregatableField("field_12", "float") + .addAggregatableField("field_21", "float") + .addAggregatableField("field_22", "float").build(); + + sourceFiltering = new FetchSourceContext(true, new String[] {"field_1*"}, null); + + ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + Tuple> fieldExtraction = extractedFieldsDetector.detect(); + + List allFields = fieldExtraction.v1().getAllFields(); + assertThat(allFields.size(), equalTo(2)); + assertThat(allFields.get(0).getName(), equalTo("field_11")); + assertThat(allFields.get(1).getName(), equalTo("field_12")); + + assertFieldSelectionContains(fieldExtraction.v2(), + FieldSelection.included("field_11", Collections.singleton("float"), false, FieldSelection.FeatureType.NUMERICAL), + FieldSelection.included("field_12", Collections.singleton("float"), false, FieldSelection.FeatureType.NUMERICAL)); + } + + public void testDetect_GivenSourceFilteringWithExcludes() { + FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() + .addAggregatableField("field_11", "float") + .addAggregatableField("field_12", "float") + .addAggregatableField("field_21", "float") + .addAggregatableField("field_22", "float").build(); + + sourceFiltering = new FetchSourceContext(true, null, new String[] {"field_1*"}); + + ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + Tuple> fieldExtraction = extractedFieldsDetector.detect(); + + List allFields = fieldExtraction.v1().getAllFields(); + assertThat(allFields.size(), equalTo(2)); + assertThat(allFields.get(0).getName(), equalTo("field_21")); + assertThat(allFields.get(1).getName(), equalTo("field_22")); + + assertFieldSelectionContains(fieldExtraction.v2(), + FieldSelection.included("field_21", Collections.singleton("float"), false, FieldSelection.FeatureType.NUMERICAL), + FieldSelection.included("field_22", Collections.singleton("float"), false, FieldSelection.FeatureType.NUMERICAL)); } - private static DataFrameAnalyticsConfig buildOutlierDetectionConfig(FetchSourceContext analyzedFields) { + private DataFrameAnalyticsConfig buildOutlierDetectionConfig() { return new DataFrameAnalyticsConfig.Builder() .setId("foo") - .setSource(new DataFrameAnalyticsSource(SOURCE_INDEX, null)) + .setSource(new DataFrameAnalyticsSource(SOURCE_INDEX, null, sourceFiltering)) .setDest(new DataFrameAnalyticsDest(DEST_INDEX, RESULTS_FIELD)) .setAnalyzedFields(analyzedFields) .setAnalysis(new OutlierDetection.Builder().build()) .build(); } - private static DataFrameAnalyticsConfig buildRegressionConfig(String dependentVariable) { - return buildRegressionConfig(dependentVariable, null); - } - - private static DataFrameAnalyticsConfig buildRegressionConfig(String dependentVariable, FetchSourceContext analyzedFields) { + private DataFrameAnalyticsConfig buildRegressionConfig(String dependentVariable) { return new DataFrameAnalyticsConfig.Builder() .setId("foo") - .setSource(new DataFrameAnalyticsSource(SOURCE_INDEX, null)) + .setSource(new DataFrameAnalyticsSource(SOURCE_INDEX, null, sourceFiltering)) .setDest(new DataFrameAnalyticsDest(DEST_INDEX, RESULTS_FIELD)) .setAnalyzedFields(analyzedFields) .setAnalysis(new Regression(dependentVariable)) .build(); } - private static DataFrameAnalyticsConfig buildClassificationConfig(String dependentVariable) { + private DataFrameAnalyticsConfig buildClassificationConfig(String dependentVariable) { return new DataFrameAnalyticsConfig.Builder() .setId("foo") - .setSource(new DataFrameAnalyticsSource(SOURCE_INDEX, null)) + .setSource(new DataFrameAnalyticsSource(SOURCE_INDEX, null, sourceFiltering)) .setDest(new DataFrameAnalyticsDest(DEST_INDEX, RESULTS_FIELD)) .setAnalysis(new Classification(dependentVariable)) .build(); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java index 0d2b5aea364eb..b1a2ba226b49f 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java @@ -71,7 +71,7 @@ public void setUpMocks() { analyticsConfig = new DataFrameAnalyticsConfig.Builder() .setId(JOB_ID) .setDescription(JOB_DESCRIPTION) - .setSource(new DataFrameAnalyticsSource(new String[] {"my_source"}, null)) + .setSource(new DataFrameAnalyticsSource(new String[] {"my_source"}, null, null)) .setDest(new DataFrameAnalyticsDest("my_dest", null)) .setAnalysis(new Regression("foo")) .build(); diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/data_frame_analytics_crud.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/data_frame_analytics_crud.yml index 6e1828efcd4ba..a1d78b7444057 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/data_frame_analytics_crud.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/data_frame_analytics_crud.yml @@ -41,7 +41,8 @@ setup: { "source": { "index": "index-source", - "query": {"term" : { "user" : "Kimchy" }} + "query": {"term" : { "user" : "Kimchy" }}, + "_source": [ "obj1.*", "obj2.*" ] }, "dest": { "index": "index-dest" @@ -1852,3 +1853,28 @@ setup: }} - is_true: create_time - is_true: version + +--- +"Test put config given analyzed_fields include field excluded by source": + + - do: + catch: /field \[excluded\] is included in \[analyzed_fields\] but not in \[source._source\]/ + ml.put_data_frame_analytics: + id: "analyzed_fields-include-field-excluded-by-source" + body: > + { + "source": { + "index": "index-source", + "query": {"term" : { "user" : "Kimchy" }}, + "_source": { + "excludes": ["excluded"] + } + }, + "dest": { + "index": "index-dest" + }, + "analysis": {"outlier_detection":{}}, + "analyzed_fields": { + "includes": ["excluded"] + } + } From f3b24bd25e24144cadd8a4de69d4955d4b0331b7 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 29 Nov 2019 14:38:06 +0200 Subject: [PATCH 025/686] [ML] Mute data frame analytics BWC tests Until #49690 is backported to 7.x --- .../test/mixed_cluster/90_ml_data_frame_analytics_crud.yml | 5 +++++ .../test/old_cluster/90_ml_data_frame_analytics_crud.yml | 3 +++ .../upgraded_cluster/90_ml_data_frame_analytics_crud.yml | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml index b0cb91c4c0f5c..8082147160718 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,3 +1,8 @@ +setup: + - skip: + version: "all" + reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" + --- "Get old outlier_detection job": diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml index fe160bba15f23..ba2cf40411672 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,4 +1,7 @@ setup: + - skip: + version: "all" + reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" - do: index: diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml index 28ec80c6373a2..462a1fd76c011 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,3 +1,8 @@ +setup: + - skip: + version: "all" + reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" + --- "Get old cluster outlier_detection job": From b13420436a7aaa64256312c2565249ad8ee7a815 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Fri, 29 Nov 2019 15:00:38 +0100 Subject: [PATCH 026/686] Replace usages of XPackPlugin with the LocalStateCompositeXPackPlugin (#49714) --- .../xpack/analytics/mapper/HistogramFieldMapperTests.java | 4 ++-- .../mapper/HistogramPercentileAggregationTests.java | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapperTests.java b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapperTests.java index 8878d86fb2051..055b01186cd61 100644 --- a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapperTests.java +++ b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapperTests.java @@ -18,7 +18,7 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.xpack.analytics.AnalyticsPlugin; -import org.elasticsearch.xpack.core.XPackPlugin; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import java.util.ArrayList; import java.util.Collection; @@ -502,7 +502,7 @@ public void testNegativeCount() throws Exception { protected Collection> getPlugins() { List> plugins = new ArrayList<>(super.getPlugins()); plugins.add(AnalyticsPlugin.class); - plugins.add(XPackPlugin.class); + plugins.add(LocalStateCompositeXPackPlugin.class); return plugins; } diff --git a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramPercentileAggregationTests.java b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramPercentileAggregationTests.java index 9561870f55495..1c826449d4a3f 100644 --- a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramPercentileAggregationTests.java +++ b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramPercentileAggregationTests.java @@ -28,7 +28,7 @@ import org.elasticsearch.search.aggregations.metrics.TDigestState; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.xpack.analytics.AnalyticsPlugin; -import org.elasticsearch.xpack.core.XPackPlugin; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import java.util.ArrayList; import java.util.Collection; @@ -225,12 +225,11 @@ public void testTDigestHistogram() throws Exception { } } - @Override protected Collection> getPlugins() { List> plugins = new ArrayList<>(super.getPlugins()); plugins.add(AnalyticsPlugin.class); - plugins.add(XPackPlugin.class); + plugins.add(LocalStateCompositeXPackPlugin.class); return plugins; } From dde2a06bc726021eb2d10c93bcc400587ccaf03e Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 29 Nov 2019 17:25:45 +0200 Subject: [PATCH 027/686] [ML] Fix DFA source filtering versions and unmute BWC tests (#49725) Relates #49690 --- .../xpack/core/ml/dataframe/DataFrameAnalyticsSource.java | 4 ++-- .../test/mixed_cluster/90_ml_data_frame_analytics_crud.yml | 5 ----- .../test/old_cluster/90_ml_data_frame_analytics_crud.yml | 3 --- .../upgraded_cluster/90_ml_data_frame_analytics_crud.yml | 5 ----- 4 files changed, 2 insertions(+), 15 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSource.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSource.java index c5e5515deb0b5..42ab45d101aa2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSource.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSource.java @@ -77,7 +77,7 @@ public DataFrameAnalyticsSource(String[] index, @Nullable QueryProvider queryPro public DataFrameAnalyticsSource(StreamInput in) throws IOException { index = in.readStringArray(); queryProvider = QueryProvider.fromStream(in); - if (in.getVersion().onOrAfter(Version.CURRENT)) { + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { sourceFiltering = in.readOptionalWriteable(FetchSourceContext::new); } else { sourceFiltering = null; @@ -95,7 +95,7 @@ public DataFrameAnalyticsSource(DataFrameAnalyticsSource other) { public void writeTo(StreamOutput out) throws IOException { out.writeStringArray(index); queryProvider.writeTo(out); - if (out.getVersion().onOrAfter(Version.CURRENT)) { + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { out.writeOptionalWriteable(sourceFiltering); } } diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml index 8082147160718..b0cb91c4c0f5c 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,8 +1,3 @@ -setup: - - skip: - version: "all" - reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" - --- "Get old outlier_detection job": diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml index ba2cf40411672..fe160bba15f23 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,7 +1,4 @@ setup: - - skip: - version: "all" - reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" - do: index: diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml index 462a1fd76c011..28ec80c6373a2 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,8 +1,3 @@ -setup: - - skip: - version: "all" - reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" - --- "Get old cluster outlier_detection job": From d62579cbe7ceef5bd6dcd7af4bc7835bed61d6f2 Mon Sep 17 00:00:00 2001 From: Marios Trivyzas Date: Fri, 29 Nov 2019 17:06:26 +0100 Subject: [PATCH 028/686] SQL: Fix issues with WEEK/ISO_WEEK/DATEDIFF (#49405) Some extended testing with MS-SQL server and H2 (which agree on results) revealed bugs in the implementation of WEEK related extraction and diff functions. Non-iso WEEK seems to be broken since #48209 because of the replacement of Calendar and the change in the ISO rules. ISO_WEEK failed for some edge cases around the January 1st. DATE_DIFF was previously based on non-iso WEEK extraction which seems not to be the case. Fixes: #49376 --- .../sql/qa/src/main/resources/date.csv-spec | 8 ++-- .../main/resources/datetime-interval.csv-spec | 5 +- .../qa/src/main/resources/datetime.csv-spec | 27 +++++++---- .../function/scalar/datetime/DateDiff.java | 17 +++---- .../scalar/datetime/DateTimeProcessor.java | 7 ++- .../datetime/NonIsoDateTimeProcessor.java | 3 +- .../datetime/DateDiffProcessorTests.java | 44 ++++++++++++++++++ .../datetime/DateTimeProcessorTests.java | 46 +++++++++++++++++++ .../NonIsoDateTimeProcessorTests.java | 44 ++++++++++++++++++ 9 files changed, 172 insertions(+), 29 deletions(-) diff --git a/x-pack/plugin/sql/qa/src/main/resources/date.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/date.csv-spec index 46557c77884e8..828d110556720 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/date.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/date.csv-spec @@ -85,12 +85,12 @@ YEAR(CAST(birth_date AS DATE)) y, birth_date, last_name l FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; d:i | dm:i | dw:i | dy:i | iso_dw:i | w:i |iso_w:i | q:i | y:i | birth_date:ts | l:s -2 |2 |4 |245 |3 |36 |35 |3 |1953 |1953-09-02T00:00:00Z |Facello -2 |2 |3 |154 |2 |23 |22 |2 |1964 |1964-06-02T00:00:00Z |Simmel +2 |2 |4 |245 |3 |36 |36 |3 |1953 |1953-09-02T00:00:00Z |Facello +2 |2 |3 |154 |2 |23 |23 |2 |1964 |1964-06-02T00:00:00Z |Simmel 3 |3 |5 |337 |4 |49 |49 |4 |1959 |1959-12-03T00:00:00Z |Bamford -1 |1 |7 |121 |6 |18 |18 |2 |1954 |1954-05-01T00:00:00Z |Koblick +1 |1 |7 |121 |6 |18 |17 |2 |1954 |1954-05-01T00:00:00Z |Koblick 21 |21 |6 |21 |5 |4 |3 |1 |1955 |1955-01-21T00:00:00Z |Maliniak -20 |20 |2 |110 |1 |17 |16 |2 |1953 |1953-04-20T00:00:00Z |Preusig +20 |20 |2 |110 |1 |17 |17 |2 |1953 |1953-04-20T00:00:00Z |Preusig 23 |23 |5 |143 |4 |21 |21 |2 |1957 |1957-05-23T00:00:00Z |Zielinski 19 |19 |4 |50 |3 |8 |8 |1 |1958 |1958-02-19T00:00:00Z |Kalloufi 19 |19 |7 |110 |6 |16 |16 |2 |1952 |1952-04-19T00:00:00Z |Peac diff --git a/x-pack/plugin/sql/qa/src/main/resources/datetime-interval.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/datetime-interval.csv-spec index 3a01c7e656563..9bb89408923b6 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/datetime-interval.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/datetime-interval.csv-spec @@ -313,7 +313,7 @@ SELECT birth_date, MAX(hire_date) - INTERVAL 1 YEAR AS f FROM test_emp GROUP BY ; monthOfDatePlusInterval_And_GroupBy -SELECT WEEK_OF_YEAR(birth_date + INTERVAL 25 YEAR) x, COUNT(*) c FROM test_emp GROUP BY x HAVING c >= 3 ORDER BY c DESC; +SELECT WEEK_OF_YEAR(birth_date + INTERVAL 25 YEAR) x, COUNT(*) c FROM test_emp GROUP BY x HAVING c >= 3 ORDER BY c DESC, x ASC; x:i | c:l ---------------+--------------- @@ -324,8 +324,7 @@ null |10 30 |4 40 |4 45 |4 -1 |3 -8 |3 +8 |3 21 |3 28 |3 32 |3 diff --git a/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec index df94a0d25c0b4..16550c3e9144e 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec @@ -110,6 +110,15 @@ SELECT WEEK(birth_date) week, birth_date FROM test_emp ORDER BY WEEK(birth_date) 44 |1961-11-02T00:00:00.000Z ; +weekOfYearVsIsoWeekOfYearEdgeCases +SELECT ISO_WEEK_OF_YEAR('2005-01-01T00:00:00.000Z'::datetime) AS "isow2005", WEEK('2005-01-01T00:00:00.000Z'::datetime) AS "w2005", +ISO_WEEK_OF_YEAR('2007-12-31T00:00:00.000Z'::datetime) AS "isow2007", WEEK('2007-12-31T00:00:00.000Z'::datetime) AS "w2007"; + + isow2005 | w2005 | isow2007 | w2007 +---------------+---------------+---------------+--------------- +53 |1 |1 |53 +; + weekOfYearWithFilter SELECT WEEK(birth_date) week, birth_date FROM test_emp WHERE WEEK(birth_date) > 50 OR WEEK(birth_date) < 4 ORDER BY WEEK(birth_date) DESC, birth_date DESC; @@ -319,7 +328,7 @@ DATEDIFF('milliseconds', '2019-09-04'::date, '2019-09-06'::date) as diff_millis, diff_year | diff_quarter | diff_month | diff_week | diff_day | diff_hours | diff_min | diff_sec | diff_millis | diff_mcsec | diff_nsec -----------+--------------+------------+-----------+----------+------------+----------+-----------+-------------+------------+---------- -9 | -91 | 269 | -611 | 11683 | -64248 | 1676160 | -14083200 | 172800000 | 0 | 0 +9 | -91 | 269 | -610 | 11683 | -64248 | 1676160 | -14083200 | 172800000 | 0 | 0 ; selectDateDiffWithField @@ -331,13 +340,13 @@ FROM test_emp WHERE emp_no >= 10032 AND emp_no <= 10042 ORDER BY 1; emp_no | birth_date | hire_date | diff_year | diff_quarter | diff_month | diff_week | diff_day | diff_min | diff_sec ---------+--------------------------+--------------------------+------------+--------------+------------+-----------+----------+-----------+---------- -10032 | 1960-08-09 00:00:00.000Z | 1990-06-20 00:00:00.000Z | 30 | -119 | 358 | -1559 | 10907 | -15706080 | 942364800 -10033 | 1956-11-14 00:00:00.000Z | 1987-03-18 00:00:00.000Z | 31 | -121 | 364 | -1584 | 11081 | -15956640 | 957398400 +10032 | 1960-08-09 00:00:00.000Z | 1990-06-20 00:00:00.000Z | 30 | -119 | 358 | -1558 | 10907 | -15706080 | 942364800 +10033 | 1956-11-14 00:00:00.000Z | 1987-03-18 00:00:00.000Z | 31 | -121 | 364 | -1583 | 11081 | -15956640 | 957398400 10034 | 1962-12-29 00:00:00.000Z | 1988-09-21 00:00:00.000Z | 26 | -103 | 309 | -1343 | 9398 | -13533120 | 811987200 -10035 | 1953-02-08 00:00:00.000Z | 1988-09-05 00:00:00.000Z | 35 | -142 | 427 | -1857 | 12993 | -18709920 | 1122595200 -10036 | 1959-08-10 00:00:00.000Z | 1992-01-03 00:00:00.000Z | 33 | -130 | 389 | -1691 | 11834 | -17040960 | 1022457600 -10037 | 1963-07-22 00:00:00.000Z | 1990-12-05 00:00:00.000Z | 27 | -109 | 329 | -1429 | 9998 | -14397120 | 863827200 -10038 | 1960-07-20 00:00:00.000Z | 1989-09-20 00:00:00.000Z | 29 | -116 | 350 | -1523 | 10654 | -15341760 | 920505600 +10035 | 1953-02-08 00:00:00.000Z | 1988-09-05 00:00:00.000Z | 35 | -142 | 427 | -1856 | 12993 | -18709920 | 1122595200 +10036 | 1959-08-10 00:00:00.000Z | 1992-01-03 00:00:00.000Z | 33 | -130 | 389 | -1690 | 11834 | -17040960 | 1022457600 +10037 | 1963-07-22 00:00:00.000Z | 1990-12-05 00:00:00.000Z | 27 | -109 | 329 | -1428 | 9998 | -14397120 | 863827200 +10038 | 1960-07-20 00:00:00.000Z | 1989-09-20 00:00:00.000Z | 29 | -116 | 350 | -1522 | 10654 | -15341760 | 920505600 10039 | 1959-10-01 00:00:00.000Z | 1988-01-19 00:00:00.000Z | 29 | -113 | 339 | -1477 | 10337 | -14885280 | 893116800 10040 | null | 1993-02-14 00:00:00.000Z | null | null | null | null | null | null | null 10041 | null | 1989-11-12 00:00:00.000Z | null | null | null | null | null | null | null @@ -451,8 +460,8 @@ SELECT count(*) as count, DATE_DIFF('weeks', birth_date, hire_date) diff FROM te count | diff ---------+------ 10 | null -1 | 1121 -1 | 1124 +1 | 1120 +1 | 1123 1 | 1168 1 | 1196 ; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateDiff.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateDiff.java index 3ccb7f66b5634..b9521cbf1880d 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateDiff.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateDiff.java @@ -25,7 +25,8 @@ import static org.elasticsearch.common.logging.LoggerMessageFormat.format; import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isDate; import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isString; -import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NonIsoDateTimeProcessor.NonIsoDateTimeExtractor; +import static org.elasticsearch.xpack.sql.util.DateUtils.DAY_IN_MILLIS; +import static org.elasticsearch.xpack.sql.util.DateUtils.UTC; public class DateDiff extends ThreeArgsDateTimeFunction { @@ -39,15 +40,11 @@ public enum Part implements DateTimeField { DAYOFYEAR((start, end) -> safeInt(diffInDays(start, end)), "dy", "y"), DAY(DAYOFYEAR::diff, "days", "dd", "d"), WEEK((start, end) -> { - int extraWeek = NonIsoDateTimeExtractor.WEEK_OF_YEAR.extract(end) - - NonIsoDateTimeExtractor.WEEK_OF_YEAR.extract(start) == 0 ? 0 : 1; - long diffWeeks = diffInDays(start, end) / 7; - if (diffWeeks < 0) { - diffWeeks -= extraWeek; - } else { - diffWeeks += extraWeek; - } - return safeInt(diffWeeks); + long startInDays = start.toInstant().toEpochMilli() / DAY_IN_MILLIS - + DatePart.Part.WEEKDAY.extract(start.withZoneSameInstant(UTC)); + long endInDays = end.toInstant().toEpochMilli() / DAY_IN_MILLIS - + DatePart.Part.WEEKDAY.extract(end.withZoneSameInstant(UTC)); + return safeInt((endInDays - startInDays) / 7); }, "weeks", "wk", "ww"), WEEKDAY(DAYOFYEAR::diff, "weekdays", "dw"), HOUR((start, end) -> safeInt(diffInHours(start, end)), "hours", "hh"), diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java index d0f7b5d9afc3a..758c1e0cd6f09 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java @@ -13,6 +13,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoField; +import java.time.temporal.WeekFields; import java.util.Objects; public class DateTimeProcessor extends BaseDateTimeProcessor { @@ -36,7 +37,11 @@ public enum DateTimeExtractor { } public int extract(ZonedDateTime dt) { - return dt.get(field); + if (field == ChronoField.ALIGNED_WEEK_OF_YEAR) { + return dt.get(WeekFields.ISO.weekOfWeekBasedYear()); + } else { + return dt.get(field); + } } public int extract(OffsetTime time) { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NonIsoDateTimeProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NonIsoDateTimeProcessor.java index 785a815a45c2a..f6f0f2ba6e135 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NonIsoDateTimeProcessor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NonIsoDateTimeProcessor.java @@ -10,7 +10,6 @@ import org.elasticsearch.common.io.stream.StreamOutput; import java.io.IOException; -import java.time.DayOfWeek; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoField; @@ -28,7 +27,7 @@ public enum NonIsoDateTimeExtractor { return dayOfWeek == 8 ? 1 : dayOfWeek; }), WEEK_OF_YEAR(zdt -> { - return zdt.get(WeekFields.of(DayOfWeek.SUNDAY, 1).weekOfWeekBasedYear()); + return zdt.get(WeekFields.SUNDAY_START.weekOfYear()); }); private final Function apply; diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateDiffProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateDiffProcessorTests.java index 7753199179346..19d329cc42415 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateDiffProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateDiffProcessorTests.java @@ -274,6 +274,17 @@ public void testDiffEdgeCases() { assertEquals(-350, new DateDiff(Source.EMPTY, l("ww"), dt2, dt1, zoneId) .makePipe().asProcessor().process(null)); + dt1 = l(dateTime(1988, 1, 2, 0, 0, 0, 0)); + dt2 = l(dateTime(1987, 12, 29, 0, 0, 0, 0)); + assertEquals(0, new DateDiff(Source.EMPTY, l("week"), dt1, dt2, UTC) + .makePipe().asProcessor().process(null)); + assertEquals(0, new DateDiff(Source.EMPTY, l("weeks"), dt2, dt1, UTC) + .makePipe().asProcessor().process(null)); + assertEquals(0, new DateDiff(Source.EMPTY, l("wk"), dt1, dt2, zoneId) + .makePipe().asProcessor().process(null)); + assertEquals(0, new DateDiff(Source.EMPTY, l("ww"), dt2, dt1, zoneId) + .makePipe().asProcessor().process(null)); + dt1 = l(dateTime(1988, 1, 5, 0, 0, 0, 0)); dt2 = l(dateTime(1996, 5, 13, 0, 0, 0, 0)); assertEquals(436, new DateDiff(Source.EMPTY, l("week"), dt1, dt2, UTC) @@ -285,6 +296,39 @@ public void testDiffEdgeCases() { assertEquals(-436, new DateDiff(Source.EMPTY, l("ww"), dt2, dt1, zoneId) .makePipe().asProcessor().process(null)); + dt1 = l(dateTime(1999, 8, 20, 0, 0, 0, 0)); + dt2 = l(dateTime(1974, 3, 17, 0, 0, 0, 0)); + assertEquals(-1326, new DateDiff(Source.EMPTY, l("week"), dt1, dt2, UTC) + .makePipe().asProcessor().process(null)); + assertEquals(1326, new DateDiff(Source.EMPTY, l("weeks"), dt2, dt1, UTC) + .makePipe().asProcessor().process(null)); + assertEquals(-1326, new DateDiff(Source.EMPTY, l("wk"), dt1, dt2, zoneId) + .makePipe().asProcessor().process(null)); + assertEquals(1326, new DateDiff(Source.EMPTY, l("ww"), dt2, dt1, zoneId) + .makePipe().asProcessor().process(null)); + + dt1 = l(dateTime(1997, 2, 2, 0, 0, 0, 0)); + dt2 = l(dateTime(1997, 9, 19, 0, 0, 0, 0)); + assertEquals(32, new DateDiff(Source.EMPTY, l("week"), dt1, dt2, UTC) + .makePipe().asProcessor().process(null)); + assertEquals(-32, new DateDiff(Source.EMPTY, l("weeks"), dt2, dt1, UTC) + .makePipe().asProcessor().process(null)); + assertEquals(32, new DateDiff(Source.EMPTY, l("wk"), dt1, dt2, zoneId) + .makePipe().asProcessor().process(null)); + assertEquals(-32, new DateDiff(Source.EMPTY, l("ww"), dt2, dt1, zoneId) + .makePipe().asProcessor().process(null)); + + dt1 = l(dateTime(1980, 11, 7, 0, 0, 0, 0)); + dt2 = l(dateTime(1979, 4, 1, 0, 0, 0, 0)); + assertEquals(-83, new DateDiff(Source.EMPTY, l("week"), dt1, dt2, UTC) + .makePipe().asProcessor().process(null)); + assertEquals(83, new DateDiff(Source.EMPTY, l("weeks"), dt2, dt1, UTC) + .makePipe().asProcessor().process(null)); + assertEquals(-83, new DateDiff(Source.EMPTY, l("wk"), dt1, dt2, zoneId) + .makePipe().asProcessor().process(null)); + assertEquals(83, new DateDiff(Source.EMPTY, l("ww"), dt2, dt1, zoneId) + .makePipe().asProcessor().process(null)); + dt1 = l(dateTime(1997, 9, 19, 0, 0, 0, 0)); dt2 = l(dateTime(2004, 8, 2, 7, 59, 23, 0)); assertEquals(60223, new DateDiff(Source.EMPTY, l("hour"), dt1, dt2, UTC) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessorTests.java index 2c5a26cc115df..4dac20646cfd2 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessorTests.java @@ -53,6 +53,29 @@ public void testApply_withTimezoneUTC() { assertEquals(1, proc.process(dateTime(0L))); assertEquals(2, proc.process(dateTime(2017, 01, 02, 10, 10))); assertEquals(31, proc.process(dateTime(2017, 01, 31, 10, 10))); + + // Tested against MS-SQL Server and H2 + proc = new DateTimeProcessor(DateTimeExtractor.ISO_WEEK_OF_YEAR, UTC); + assertEquals(1, proc.process(dateTime(1988, 1, 5, 0, 0, 0, 0))); + assertEquals(5, proc.process(dateTime(2001, 2, 4, 0, 0, 0, 0))); + assertEquals(6, proc.process(dateTime(1977, 2, 8, 0, 0, 0, 0))); + assertEquals(11, proc.process(dateTime(1974, 3, 17, 0, 0, 0, 0))); + assertEquals(16, proc.process(dateTime(1977, 4, 20, 0, 0, 0, 0))); + assertEquals(16, proc.process(dateTime(1994, 4, 20, 0, 0, 0, 0))); + assertEquals(17, proc.process(dateTime(2002, 4, 27, 0, 0, 0, 0))); + assertEquals(18, proc.process(dateTime(1974, 5, 3, 0, 0, 0, 0))); + assertEquals(22, proc.process(dateTime(1997, 5, 30, 0, 0, 0, 0))); + assertEquals(22, proc.process(dateTime(1995, 6, 4, 0, 0, 0, 0))); + assertEquals(28, proc.process(dateTime(1972, 7, 12, 0, 0, 0, 0))); + assertEquals(30, proc.process(dateTime(1980, 7, 26, 0, 0, 0, 0))); + assertEquals(33, proc.process(dateTime(1998, 8, 12, 0, 0, 0, 0))); + assertEquals(35, proc.process(dateTime(1995, 9, 3, 0, 0, 0, 0))); + assertEquals(37, proc.process(dateTime(1976, 9, 9, 0, 0, 0, 0))); + assertEquals(38, proc.process(dateTime(1997, 9, 19, 0, 0, 0, 0))); + assertEquals(45, proc.process(dateTime(1980, 11, 7, 0, 0, 0, 0))); + assertEquals(53, proc.process(dateTime(2005, 1, 1, 0, 0, 0, 0))); + assertEquals(1, proc.process(dateTime(2007, 12, 31, 0, 0, 0, 0))); + assertEquals(1, proc.process(dateTime(2019, 12, 31, 20, 22, 33, 987654321))); } public void testApply_withTimezoneOtherThanUTC() { @@ -62,6 +85,29 @@ public void testApply_withTimezoneOtherThanUTC() { proc = new DateTimeProcessor(DateTimeExtractor.DAY_OF_MONTH, zoneId); assertEquals(1, proc.process(dateTime(2017, 12, 31, 20, 30))); + + // Tested against MS-SQL Server and H2 + proc = new DateTimeProcessor(DateTimeExtractor.ISO_WEEK_OF_YEAR, UTC); + assertEquals(1, proc.process(dateTime(1988, 1, 5, 0, 0, 0, 0))); + assertEquals(5, proc.process(dateTime(2001, 2, 4, 0, 0, 0, 0))); + assertEquals(6, proc.process(dateTime(1977, 2, 8, 0, 0, 0, 0))); + assertEquals(11, proc.process(dateTime(1974, 3, 17, 0, 0, 0, 0))); + assertEquals(16, proc.process(dateTime(1977, 4, 20, 0, 0, 0, 0))); + assertEquals(16, proc.process(dateTime(1994, 4, 20, 0, 0, 0, 0))); + assertEquals(17, proc.process(dateTime(2002, 4, 27, 0, 0, 0, 0))); + assertEquals(18, proc.process(dateTime(1974, 5, 3, 0, 0, 0, 0))); + assertEquals(22, proc.process(dateTime(1997, 5, 30, 0, 0, 0, 0))); + assertEquals(22, proc.process(dateTime(1995, 6, 4, 0, 0, 0, 0))); + assertEquals(28, proc.process(dateTime(1972, 7, 12, 0, 0, 0, 0))); + assertEquals(30, proc.process(dateTime(1980, 7, 26, 0, 0, 0, 0))); + assertEquals(33, proc.process(dateTime(1998, 8, 12, 0, 0, 0, 0))); + assertEquals(35, proc.process(dateTime(1995, 9, 3, 0, 0, 0, 0))); + assertEquals(37, proc.process(dateTime(1976, 9, 9, 0, 0, 0, 0))); + assertEquals(38, proc.process(dateTime(1997, 9, 19, 0, 0, 0, 0))); + assertEquals(45, proc.process(dateTime(1980, 11, 7, 0, 0, 0, 0))); + assertEquals(53, proc.process(dateTime(2005, 1, 1, 0, 0, 0, 0))); + assertEquals(1, proc.process(dateTime(2007, 12, 31, 0, 0, 0, 0))); + assertEquals(1, proc.process(dateTime(2019, 12, 31, 20, 22, 33, 987654321))); } public void testFailOnTime() { diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NonIsoDateTimeProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NonIsoDateTimeProcessorTests.java index a1971cecad1d5..907c60709a2da 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NonIsoDateTimeProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NonIsoDateTimeProcessorTests.java @@ -55,6 +55,28 @@ public void testNonISOWeekOfYearInUTC() { assertEquals(17, proc.process(dateTime(766833730000L))); //1994-04-20T09:22:10Z[UTC] assertEquals(29, proc.process(dateTime(79780930000L))); //1972-07-12T09:22:10Z[UTC] assertEquals(33, proc.process(dateTime(902913730000L))); //1998-08-12T09:22:10Z[UTC] + + // Tested against MS-SQL Server and H2 + assertEquals(2, proc.process(dateTime(1988, 1, 5, 0, 0, 0, 0))); + assertEquals(6, proc.process(dateTime(2001, 2, 4, 0, 0, 0, 0))); + assertEquals(7, proc.process(dateTime(1977, 2, 8, 0, 0, 0, 0))); + assertEquals(12, proc.process(dateTime(1974, 3, 17, 0, 0, 0, 0))); + assertEquals(17, proc.process(dateTime(1977, 4, 20, 0, 0, 0, 0))); + assertEquals(17, proc.process(dateTime(1994, 4, 20, 0, 0, 0, 0))); + assertEquals(17, proc.process(dateTime(2002, 4, 27, 0, 0, 0, 0))); + assertEquals(18, proc.process(dateTime(1974, 5, 3, 0, 0, 0, 0))); + assertEquals(22, proc.process(dateTime(1997, 5, 30, 0, 0, 0, 0))); + assertEquals(23, proc.process(dateTime(1995, 6, 4, 0, 0, 0, 0))); + assertEquals(29, proc.process(dateTime(1972, 7, 12, 0, 0, 0, 0))); + assertEquals(30, proc.process(dateTime(1980, 7, 26, 0, 0, 0, 0))); + assertEquals(33, proc.process(dateTime(1998, 8, 12, 0, 0, 0, 0))); + assertEquals(36, proc.process(dateTime(1995, 9, 3, 0, 0, 0, 0))); + assertEquals(37, proc.process(dateTime(1976, 9, 9, 0, 0, 0, 0))); + assertEquals(38, proc.process(dateTime(1997, 9, 19, 0, 0, 0, 0))); + assertEquals(45, proc.process(dateTime(1980, 11, 7, 0, 0, 0, 0))); + assertEquals(1, proc.process(dateTime(2005, 1, 1, 0, 0, 0, 0))); + assertEquals(53, proc.process(dateTime(2007, 12, 31, 0, 0, 0, 0))); + assertEquals(53, proc.process(dateTime(2019, 12, 31, 20, 22, 33, 987654321))); } public void testNonISOWeekOfYearInNonUTCTimeZone() { @@ -68,6 +90,28 @@ public void testNonISOWeekOfYearInNonUTCTimeZone() { assertEquals(17, proc.process(dateTime(766833730000L))); assertEquals(29, proc.process(dateTime(79780930000L))); assertEquals(33, proc.process(dateTime(902913730000L))); + + // Tested against MS-SQL Server and H2 + assertEquals(2, proc.process(dateTime(1988, 1, 5, 0, 0, 0, 0))); + assertEquals(5, proc.process(dateTime(2001, 2, 4, 0, 0, 0, 0))); + assertEquals(7, proc.process(dateTime(1977, 2, 8, 0, 0, 0, 0))); + assertEquals(11, proc.process(dateTime(1974, 3, 17, 0, 0, 0, 0))); + assertEquals(17, proc.process(dateTime(1977, 4, 20, 0, 0, 0, 0))); + assertEquals(17, proc.process(dateTime(1994, 4, 20, 0, 0, 0, 0))); + assertEquals(17, proc.process(dateTime(2002, 4, 27, 0, 0, 0, 0))); + assertEquals(18, proc.process(dateTime(1974, 5, 3, 0, 0, 0, 0))); + assertEquals(22, proc.process(dateTime(1997, 5, 30, 0, 0, 0, 0))); + assertEquals(22, proc.process(dateTime(1995, 6, 4, 0, 0, 0, 0))); + assertEquals(29, proc.process(dateTime(1972, 7, 12, 0, 0, 0, 0))); + assertEquals(30, proc.process(dateTime(1980, 7, 26, 0, 0, 0, 0))); + assertEquals(33, proc.process(dateTime(1998, 8, 12, 0, 0, 0, 0))); + assertEquals(35, proc.process(dateTime(1995, 9, 3, 0, 0, 0, 0))); + assertEquals(37, proc.process(dateTime(1976, 9, 9, 0, 0, 0, 0))); + assertEquals(38, proc.process(dateTime(1997, 9, 19, 0, 0, 0, 0))); + assertEquals(45, proc.process(dateTime(1980, 11, 7, 0, 0, 0, 0))); + assertEquals(53, proc.process(dateTime(2005, 1, 1, 0, 0, 0, 0))); + assertEquals(53, proc.process(dateTime(2007, 12, 31, 0, 0, 0, 0))); + assertEquals(53, proc.process(dateTime(2019, 12, 31, 20, 22, 33, 987654321))); } public void testNonISODayOfWeekInUTC() { From 9866ca084f38c1c01f15e2f5b2eab552cc8c9f9e Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Fri, 29 Nov 2019 17:46:44 +0100 Subject: [PATCH 029/686] Deprecate sorting in reindex (#49458) Reindex sort never gave a guarantee about the order of documents being indexed into the destination, though it could give a sense of locality of source data. It prevents us from doing resilient reindex and other optimizations and it has therefore been deprecated. Related to #47567 --- .../documentation/CRUDDocumentationIT.java | 5 -- .../high-level/document/reindex.asciidoc | 10 ---- docs/reference/docs/reindex.asciidoc | 42 ++++--------- .../ilm/ilm-with-existing-indices.asciidoc | 6 +- .../index/reindex/Reindexer.java | 9 +++ .../index/reindex/ReindexSingleNodeTests.java | 60 +++++++++++++++++++ .../rest-api-spec/test/reindex/30_search.yml | 8 ++- .../index/reindex/ReindexRequest.java | 3 + 8 files changed, 95 insertions(+), 48 deletions(-) create mode 100644 modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexSingleNodeTests.java diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java index 9d88bb2ff222a..b579b81e2eea5 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java @@ -83,7 +83,6 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; -import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.tasks.TaskId; import java.util.Collections; @@ -833,10 +832,6 @@ public void testReindex() throws Exception { // tag::reindex-request-pipeline request.setDestPipeline("my_pipeline"); // <1> // end::reindex-request-pipeline - // tag::reindex-request-sort - request.addSortField("field1", SortOrder.DESC); // <1> - request.addSortField("field2", SortOrder.ASC); // <2> - // end::reindex-request-sort // tag::reindex-request-script request.setScript( new Script( diff --git a/docs/java-rest/high-level/document/reindex.asciidoc b/docs/java-rest/high-level/document/reindex.asciidoc index f30b9eef4a41b..c094a5f1ab7eb 100644 --- a/docs/java-rest/high-level/document/reindex.asciidoc +++ b/docs/java-rest/high-level/document/reindex.asciidoc @@ -89,16 +89,6 @@ include-tagged::{doc-tests-file}[{api}-request-pipeline] -------------------------------------------------- <1> set pipeline to `my_pipeline` -If you want a particular set of documents from the source index you’ll need to use sort. If possible, prefer a more -selective query to maxDocs and sort. - -["source","java",subs="attributes,callouts,macros"] --------------------------------------------------- -include-tagged::{doc-tests-file}[{api}-request-sort] --------------------------------------------------- -<1> add descending sort to`field1` -<2> add ascending sort to `field2` - +{request}+ also supports a `script` that modifies the document. It allows you to also change the document's metadata. The following example illustrates that. diff --git a/docs/reference/docs/reindex.asciidoc b/docs/reference/docs/reindex.asciidoc index 67c54d37f15d4..c5ba1d36743fc 100644 --- a/docs/reference/docs/reindex.asciidoc +++ b/docs/reference/docs/reindex.asciidoc @@ -476,9 +476,14 @@ which defaults to a maximum size of 100 MB. (Optional, integer) Total number of slices. `sort`::: ++ +-- (Optional, list) A comma-separated list of `:` pairs to sort by before indexing. Use in conjunction with `max_docs` to control what documents are reindexed. +deprecated::[7.6, Sort in reindex is deprecated. Sorting in reindex was never guaranteed to index documents in order and prevents further development of reindex such as resilience and performance improvements. If used in combination with `max_docs`, consider using a query filter instead.] +-- + `_source`::: (Optional, string) If `true` reindexes all source fields. Set to a list to reindex select fields. @@ -602,8 +607,8 @@ POST _reindex -------------------------------------------------- // TEST[setup:twitter] -[[docs-reindex-select-sort]] -===== Reindex select documents with sort +[[docs-reindex-select-max-docs]] +===== Reindex select documents with `max_docs` You can limit the number of processed documents by setting `max_docs`. For example, this request copies a single document from `twitter` to @@ -624,28 +629,6 @@ POST _reindex -------------------------------------------------- // TEST[setup:twitter] -You can use `sort` in conjunction with `max_docs` to select the documents you want to reindex. -Sorting makes the scroll less efficient but in some contexts it's worth it. -If possible, it's better to use a more selective query instead of `max_docs` and `sort`. - -For example, following request copies 10000 documents from `twitter` into `new_twitter`: - -[source,console] --------------------------------------------------- -POST _reindex -{ - "max_docs": 10000, - "source": { - "index": "twitter", - "sort": { "date": "desc" } - }, - "dest": { - "index": "new_twitter" - } -} --------------------------------------------------- -// TEST[setup:twitter] - [[docs-reindex-multiple-indices]] ===== Reindex from multiple indices @@ -824,11 +807,10 @@ POST _reindex "index": "twitter", "query": { "function_score" : { - "query" : { "match_all": {} }, - "random_score" : {} + "random_score" : {}, + "min_score" : 0.9 <1> } - }, - "sort": "_score" <1> + } }, "dest": { "index": "random_twitter" @@ -837,8 +819,8 @@ POST _reindex ---------------------------------------------------------------- // TEST[setup:big_twitter] -<1> `_reindex` defaults to sorting by `_doc` so `random_score` will not have any -effect unless you override the sort to `_score`. +<1> You may need to adjust the `min_score` depending on the relative amount of +data extracted from source. [[reindex-scripts]] ===== Modify documents during reindexing diff --git a/docs/reference/ilm/ilm-with-existing-indices.asciidoc b/docs/reference/ilm/ilm-with-existing-indices.asciidoc index cff82df838806..96c1e4589de9a 100644 --- a/docs/reference/ilm/ilm-with-existing-indices.asciidoc +++ b/docs/reference/ilm/ilm-with-existing-indices.asciidoc @@ -352,6 +352,10 @@ will mean that all documents in `ilm-mylogs-000001` come before all documents in `ilm-mylogs-000002`, and so on. However, if this is not a requirement, omitting the sort will allow the data to be reindexed more quickly. +NOTE: Sorting in reindex is deprecated, see +<>. Instead use timestamp +ranges to partition data in separate reindex runs. + IMPORTANT: If your data uses document IDs generated by means other than Elasticsearch's automatic ID generation, you may need to do additional processing to ensure that the document IDs don't conflict during the reindex, as @@ -404,4 +408,4 @@ PUT _cluster/settings All of the reindexed data should now be accessible via the alias set up above, in this case `mylogs`. Once you have verified that all the data has been reindexed and is available in the new indices, the existing indices can be -safely removed. \ No newline at end of file +safely removed. diff --git a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/Reindexer.java b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/Reindexer.java index 39879845bdca1..cbe0e8be6572c 100644 --- a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/Reindexer.java +++ b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/Reindexer.java @@ -40,6 +40,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -51,6 +52,7 @@ import org.elasticsearch.index.reindex.remote.RemoteScrollableHitSource; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptService; +import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; @@ -71,6 +73,9 @@ public class Reindexer { private static final Logger logger = LogManager.getLogger(Reindexer.class); + private static final DeprecationLogger deprecationLogger = new DeprecationLogger(logger); + static final String SORT_DEPRECATED_MESSAGE = "The sort option in reindex is deprecated. " + + "Instead consider using query filtering to find the desired subset of data."; private final ClusterService clusterService; private final Client client; @@ -88,6 +93,10 @@ public class Reindexer { } public void initTask(BulkByScrollTask task, ReindexRequest request, ActionListener listener) { + SearchSourceBuilder searchSource = request.getSearchRequest().source(); + if (searchSource != null && searchSource.sorts() != null && searchSource.sorts().isEmpty() == false) { + deprecationLogger.deprecated(SORT_DEPRECATED_MESSAGE); + } BulkByScrollParallelizationHelper.initTaskState(task, request, client, listener); } diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexSingleNodeTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexSingleNodeTests.java new file mode 100644 index 0000000000000..14b68ebdf6c6d --- /dev/null +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexSingleNodeTests.java @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package org.elasticsearch.index.reindex; + +import org.elasticsearch.index.query.RangeQueryBuilder; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.search.sort.SortOrder; +import org.elasticsearch.test.ESSingleNodeTestCase; + +import java.util.Arrays; +import java.util.Collection; + +import static org.elasticsearch.index.reindex.ReindexTestCase.matcher; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; + +public class ReindexSingleNodeTests extends ESSingleNodeTestCase { + @Override + protected Collection> getPlugins() { + return Arrays.asList(ReindexPlugin.class); + } + + public void testDeprecatedSort() { + int max = between(2, 20); + for (int i = 0; i < max; i++) { + client().prepareIndex("source").setId(Integer.toString(i)).setSource("foo", i).get(); + } + + client().admin().indices().prepareRefresh("source").get(); + assertHitCount(client().prepareSearch("source").setSize(0).get(), max); + + // Copy a subset of the docs sorted + int subsetSize = randomIntBetween(1, max - 1); + ReindexRequestBuilder copy = new ReindexRequestBuilder(client(), ReindexAction.INSTANCE) + .source("source").destination("dest").refresh(true); + copy.maxDocs(subsetSize); + copy.request().addSortField("foo", SortOrder.DESC); + assertThat(copy.get(), matcher().created(subsetSize)); + + assertHitCount(client().prepareSearch("dest").setSize(0).get(), subsetSize); + assertHitCount(client().prepareSearch("dest").setQuery(new RangeQueryBuilder("foo").gte(0).lt(max-subsetSize)).get(), 0); + assertWarnings(Reindexer.SORT_DEPRECATED_MESSAGE); + } +} diff --git a/modules/reindex/src/test/resources/rest-api-spec/test/reindex/30_search.yml b/modules/reindex/src/test/resources/rest-api-spec/test/reindex/30_search.yml index 709b9c0d17340..be2d943431030 100644 --- a/modules/reindex/src/test/resources/rest-api-spec/test/reindex/30_search.yml +++ b/modules/reindex/src/test/resources/rest-api-spec/test/reindex/30_search.yml @@ -77,8 +77,9 @@ --- "Sorting and max_docs in body combined": - skip: - version: " - 7.2.99" - reason: "max_docs introduced in 7.3.0" + version: " - 7.5.99" + reason: "max_docs introduced in 7.3.0, but sort deprecated in 7.6" + features: "warnings" - do: index: @@ -94,6 +95,9 @@ indices.refresh: {} - do: + warnings: + - The sort option in reindex is deprecated. Instead consider using query + filtering to find the desired subset of data. reindex: refresh: true body: diff --git a/server/src/main/java/org/elasticsearch/index/reindex/ReindexRequest.java b/server/src/main/java/org/elasticsearch/index/reindex/ReindexRequest.java index b4991697b1742..c01edd25b24ab 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/ReindexRequest.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/ReindexRequest.java @@ -176,7 +176,10 @@ public ReindexRequest setSourceQuery(QueryBuilder queryBuilder) { * * @param name The name of the field to sort by * @param order The order in which to sort + * @deprecated Specifying a sort field for reindex is deprecated. If using this in combination with maxDocs, consider using a + * query filter instead. */ + @Deprecated public ReindexRequest addSortField(String name, SortOrder order) { this.getSearchRequest().source().sort(name, order); return this; From 828a1a8491c2436c133254eae766de9c774e87da Mon Sep 17 00:00:00 2001 From: Tugberk Ugurlu Date: Fri, 29 Nov 2019 17:42:02 +0000 Subject: [PATCH 030/686] [Docs] Fix typo in templates.asciidoc (#49726) --- docs/reference/indices/templates.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/indices/templates.asciidoc b/docs/reference/indices/templates.asciidoc index e31b34d05b1b6..db2cc265777b2 100644 --- a/docs/reference/indices/templates.asciidoc +++ b/docs/reference/indices/templates.asciidoc @@ -59,7 +59,7 @@ override any settings or mappings specified in an index template. ===== Comments in index templates You can use C-style /* */ block comments in index templates. -You can includes comments anywhere in the request body, +You can include comments anywhere in the request body, except before the opening curly bracket. [[getting]] From 5d1422ded8fe1418505d14cd0e70430c593ea57c Mon Sep 17 00:00:00 2001 From: Mayya Sharipova Date: Fri, 29 Nov 2019 14:32:17 -0500 Subject: [PATCH 031/686] Add bulkScorer to script score query (#46336) Some queries return bulk scorers that can be significantly faster than iterating naively over the scorer. By giving script_score a BulkScorer that would delegate to the wrapped query, we could make it faster in some cases. Closes #40837 --- .../search/function/ScriptScoreQuery.java | 179 ++++++++++++++---- .../search/query/ScriptScoreQueryIT.java | 29 +++ 2 files changed, 170 insertions(+), 38 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreQuery.java b/server/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreQuery.java index 17b5d87e7cb05..d285deff3a289 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreQuery.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreQuery.java @@ -25,12 +25,17 @@ import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.Explanation; -import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.FilterLeafCollector; +import org.apache.lucene.search.LeafCollector; import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorable; +import org.apache.lucene.search.Weight; +import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.QueryVisitor; import org.apache.lucene.search.ScoreMode; import org.apache.lucene.search.Scorer; -import org.apache.lucene.search.Weight; +import org.apache.lucene.search.BulkScorer; +import org.apache.lucene.util.Bits; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.script.ScoreScript; @@ -83,6 +88,19 @@ public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float bo Weight subQueryWeight = subQuery.createWeight(searcher, subQueryScoreMode, boost); return new Weight(this){ + @Override + public BulkScorer bulkScorer(LeafReaderContext context) throws IOException { + if (minScore == null) { + final BulkScorer subQueryBulkScorer = subQueryWeight.bulkScorer(context); + if (subQueryBulkScorer == null) { + return null; + } + return new ScriptScoreBulkScorer(subQueryBulkScorer, subQueryScoreMode, makeScoreScript(context)); + } else { + return super.bulkScorer(context); + } + } + @Override public void extractTerms(Set terms) { subQueryWeight.extractTerms(terms); @@ -94,8 +112,7 @@ public Scorer scorer(LeafReaderContext context) throws IOException { if (subQueryScorer == null) { return null; } - Scorer scriptScorer = makeScriptScorer(subQueryScorer, context, null); - + Scorer scriptScorer = new ScriptScorer(this, makeScoreScript(context), subQueryScorer, subQueryScoreMode, null); if (minScore != null) { scriptScorer = new MinScoreScorer(this, scriptScorer, minScore); } @@ -109,7 +126,8 @@ public Explanation explain(LeafReaderContext context, int doc) throws IOExceptio return subQueryExplanation; } ExplanationHolder explanationHolder = new ExplanationHolder(); - Scorer scorer = makeScriptScorer(subQueryWeight.scorer(context), context, explanationHolder); + Scorer scorer = new ScriptScorer(this, makeScoreScript(context), + subQueryWeight.scorer(context), subQueryScoreMode, explanationHolder); int newDoc = scorer.iterator().advance(doc); assert doc == newDoc; // subquery should have already matched above float score = scorer.score(); @@ -132,42 +150,13 @@ public Explanation explain(LeafReaderContext context, int doc) throws IOExceptio } return explanation; } - - private Scorer makeScriptScorer(Scorer subQueryScorer, LeafReaderContext context, - ExplanationHolder explanation) throws IOException { + + private ScoreScript makeScoreScript(LeafReaderContext context) throws IOException { final ScoreScript scoreScript = scriptBuilder.newInstance(context); - scoreScript.setScorer(subQueryScorer); scoreScript._setIndexName(indexName); scoreScript._setShard(shardId); scoreScript._setIndexVersion(indexVersion); - - return new Scorer(this) { - @Override - public float score() throws IOException { - int docId = docID(); - scoreScript.setDocument(docId); - float score = (float) scoreScript.execute(explanation); - if (score == Float.NEGATIVE_INFINITY || Float.isNaN(score)) { - throw new ElasticsearchException( - "script score query returned an invalid score: " + score + " for doc: " + docId); - } - return score; - } - @Override - public int docID() { - return subQueryScorer.docID(); - } - - @Override - public DocIdSetIterator iterator() { - return subQueryScorer.iterator(); - } - - @Override - public float getMaxScore(int upTo) { - return Float.MAX_VALUE; // TODO: what would be a good upper bound? - } - }; + return scoreScript; } @Override @@ -187,7 +176,7 @@ public void visit(QueryVisitor visitor) { @Override public String toString(String field) { StringBuilder sb = new StringBuilder(); - sb.append("script score (").append(subQuery.toString(field)).append(", script: "); + sb.append("script_score (").append(subQuery.toString(field)).append(", script: "); sb.append("{" + script.toString() + "}"); return sb.toString(); } @@ -209,4 +198,118 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(subQuery, script, minScore, indexName, shardId, indexVersion); } + + + private static class ScriptScorer extends Scorer { + private final ScoreScript scoreScript; + private final Scorer subQueryScorer; + private final ExplanationHolder explanation; + + ScriptScorer(Weight weight, ScoreScript scoreScript, Scorer subQueryScorer, + ScoreMode subQueryScoreMode, ExplanationHolder explanation) { + super(weight); + this.scoreScript = scoreScript; + if (subQueryScoreMode == ScoreMode.COMPLETE) { + scoreScript.setScorer(subQueryScorer); + } + this.subQueryScorer = subQueryScorer; + this.explanation = explanation; + } + + @Override + public float score() throws IOException { + int docId = docID(); + scoreScript.setDocument(docId); + float score = (float) scoreScript.execute(explanation); + if (score == Float.NEGATIVE_INFINITY || Float.isNaN(score)) { + throw new ElasticsearchException( + "script_score query returned an invalid score [" + score + "] for doc [" + docId + "]."); + } + return score; + } + @Override + public int docID() { + return subQueryScorer.docID(); + } + + @Override + public DocIdSetIterator iterator() { + return subQueryScorer.iterator(); + } + + @Override + public float getMaxScore(int upTo) { + return Float.MAX_VALUE; // TODO: what would be a good upper bound? + } + + } + + private static class ScriptScorable extends Scorable { + private final ScoreScript scoreScript; + private final Scorable subQueryScorer; + private final ExplanationHolder explanation; + + ScriptScorable(ScoreScript scoreScript, Scorable subQueryScorer, + ScoreMode subQueryScoreMode, ExplanationHolder explanation) { + this.scoreScript = scoreScript; + if (subQueryScoreMode == ScoreMode.COMPLETE) { + scoreScript.setScorer(subQueryScorer); + } + this.subQueryScorer = subQueryScorer; + this.explanation = explanation; + } + + @Override + public float score() throws IOException { + int docId = docID(); + scoreScript.setDocument(docId); + float score = (float) scoreScript.execute(explanation); + if (score == Float.NEGATIVE_INFINITY || Float.isNaN(score)) { + throw new ElasticsearchException( + "script_score query returned an invalid score [" + score + "] for doc [" + docId + "]."); + } + return score; + } + @Override + public int docID() { + return subQueryScorer.docID(); + } + } + + /** + * Use the {@link BulkScorer} of the sub-query, + * as it may be significantly faster (e.g. BooleanScorer) than iterating over the scorer + */ + private static class ScriptScoreBulkScorer extends BulkScorer { + private final BulkScorer subQueryBulkScorer; + private final ScoreMode subQueryScoreMode; + private final ScoreScript scoreScript; + + ScriptScoreBulkScorer(BulkScorer subQueryBulkScorer, ScoreMode subQueryScoreMode, ScoreScript scoreScript) { + this.subQueryBulkScorer = subQueryBulkScorer; + this.subQueryScoreMode = subQueryScoreMode; + this.scoreScript = scoreScript; + } + + @Override + public int score(LeafCollector collector, Bits acceptDocs, int min, int max) throws IOException { + return subQueryBulkScorer.score(wrapCollector(collector), acceptDocs, min, max); + } + + private LeafCollector wrapCollector(LeafCollector collector) { + return new FilterLeafCollector(collector) { + @Override + public void setScorer(Scorable scorer) throws IOException { + in.setScorer(new ScriptScorable(scoreScript, scorer, subQueryScoreMode, null)); + } + }; + } + + @Override + public long cost() { + return subQueryBulkScorer.cost(); + } + + } + } diff --git a/server/src/test/java/org/elasticsearch/search/query/ScriptScoreQueryIT.java b/server/src/test/java/org/elasticsearch/search/query/ScriptScoreQueryIT.java index a6ee658ac5d67..71380ce9fa303 100644 --- a/server/src/test/java/org/elasticsearch/search/query/ScriptScoreQueryIT.java +++ b/server/src/test/java/org/elasticsearch/search/query/ScriptScoreQueryIT.java @@ -22,6 +22,7 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.RangeQueryBuilder; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.script.MockScriptPlugin; @@ -35,6 +36,7 @@ import java.util.Map; import java.util.function.Function; +import static org.elasticsearch.index.query.QueryBuilders.boolQuery; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; import static org.elasticsearch.index.query.QueryBuilders.scriptScoreQuery; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -104,6 +106,33 @@ public void testScriptScore() { assertOrderedSearchHits(resp, "10", "8", "6"); } + public void testScriptScoreBoolQuery() { + assertAcked( + prepareCreate("test-index").addMapping("_doc", "field1", "type=text", "field2", "type=double") + ); + int docCount = 10; + for (int i = 1; i <= docCount; i++) { + client().prepareIndex("test-index").setId("" + i) + .setSource("field1", "text" + i, "field2", i) + .get(); + } + refresh(); + + Map params = new HashMap<>(); + params.put("param1", 0.1); + Script script = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "doc['field2'].value * param1", params); + QueryBuilder boolQuery = boolQuery().should(matchQuery("field1", "text1")).should(matchQuery("field1", "text10")); + SearchResponse resp = client() + .prepareSearch("test-index") + .setQuery(scriptScoreQuery(boolQuery, script)) + .get(); + assertNoFailures(resp); + assertOrderedSearchHits(resp, "10", "1"); + assertFirstHit(resp, hasScore(1.0f)); + assertSecondHit(resp, hasScore(0.1f)); + } + + // test that when the internal query is rewritten script_score works well public void testRewrittenQuery() { assertAcked( From 836f97662f68aa0ae041beb29a2b602c51a87a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Mon, 2 Dec 2019 11:21:34 +0100 Subject: [PATCH 032/686] Add note how to run locale sensitive unit test (#49491) Some unit test checking locale sensitive functionality require the -Djava.locale.providers=SPI,COMPAT flag to be set. When running tests though gradle we pass this already to the BuildPlugin, but running from the IDE this might need to be set manually. Adding a note explaining this to the CONTRIBUTING.md doc and leaving a note in the test comment of SearchQueryIT.testRangeQueryWithLocaleMapping which is a test we know that suffers from this issue. --- CONTRIBUTING.md | 4 ++++ .../org/elasticsearch/search/query/SearchQueryIT.java | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c4da6a22d985..24d9b4a87c461 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -153,6 +153,10 @@ For IntelliJ, go to For Eclipse, go to `Preferences->Java->Installed JREs` and add `-ea` to `VM Arguments`. +Some tests related to locale testing also require the flag +`-Djava.locale.providers` to be set. Set the VM options/VM arguments for +IntelliJ or Eclipse like describe above to use +`-Djava.locale.providers=SPI,COMPAT`. ### Java Language Formatting Guidelines diff --git a/server/src/test/java/org/elasticsearch/search/query/SearchQueryIT.java b/server/src/test/java/org/elasticsearch/search/query/SearchQueryIT.java index fe24db5161d1c..395302bc4c7bb 100644 --- a/server/src/test/java/org/elasticsearch/search/query/SearchQueryIT.java +++ b/server/src/test/java/org/elasticsearch/search/query/SearchQueryIT.java @@ -1569,8 +1569,18 @@ public void testRangeQueryWithTimeZone() throws Exception { assertThat(searchResponse.getHits().getAt(0).getId(), is("4")); } + /** + * Test range with a custom locale, e.g. "de" in this case. Documents here mention the day of week + * as "Mi" for "Mittwoch (Wednesday" and "Do" for "Donnerstag (Thursday)" and the month in the query + * as "Dez" for "Dezember (December)". + * Note: this test currently needs the JVM arg `-Djava.locale.providers=SPI,COMPAT` to be set. + * When running with gradle this is done implicitly through the BuildPlugin, but when running from + * an IDE this might need to be set manually in the run configuration. See also CONTRIBUTING.md section + * on "Configuring IDEs And Running Tests". + */ public void testRangeQueryWithLocaleMapping() throws Exception { assumeTrue("need java 9 for testing ",JavaVersion.current().compareTo(JavaVersion.parse("9")) >= 0); + assert ("SPI,COMPAT".equals(System.getProperty("java.locale.providers"))) : "`-Djava.locale.providers=SPI,COMPAT` needs to be set"; assertAcked(prepareCreate("test") .addMapping("type1", jsonBuilder().startObject().startObject("properties").startObject("date_field") From fd7b9a9ddd8d1f4e8b71ca281153a964c1c4c308 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 2 Dec 2019 12:43:18 +0000 Subject: [PATCH 033/686] Drop snapshot instructions for autobootstrap fix (#49755) The "Restore any snapshots as required" step is a trap: it's somewhere between tricky and impossible to restore multiple clusters into a single one. Also add a note about configuring discovery during a rolling upgrade to proscribe any rare cases where you might accidentally autobootstrap during the upgrade. --- docs/reference/modules/discovery/bootstrapping.asciidoc | 4 ---- docs/reference/upgrade/rolling_upgrade.asciidoc | 4 +++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/reference/modules/discovery/bootstrapping.asciidoc b/docs/reference/modules/discovery/bootstrapping.asciidoc index ba6cdc7127492..7d1244de2568b 100644 --- a/docs/reference/modules/discovery/bootstrapping.asciidoc +++ b/docs/reference/modules/discovery/bootstrapping.asciidoc @@ -138,14 +138,10 @@ clusters together without a risk of data loss. You can tell that you have formed separate clusters by checking the cluster UUID reported by `GET /` on each node. If you intended to form a single cluster then you should start again: -* Take a <> of each of the single-host clusters if - you do not want to lose any data that they hold. Note that each cluster must - use its own snapshot repository. * Shut down all the nodes. * Completely wipe each node by deleting the contents of their <>. * Configure `cluster.initial_master_nodes` as described above. * Restart all the nodes and verify that they have formed a single cluster. -* <> any snapshots as required. ================================================== diff --git a/docs/reference/upgrade/rolling_upgrade.asciidoc b/docs/reference/upgrade/rolling_upgrade.asciidoc index 348672546eed5..96b3d12fb0264 100644 --- a/docs/reference/upgrade/rolling_upgrade.asciidoc +++ b/docs/reference/upgrade/rolling_upgrade.asciidoc @@ -68,7 +68,9 @@ include::set-paths-tip.asciidoc[] [[rolling-upgrades-bootstrapping]] NOTE: You should leave `cluster.initial_master_nodes` unset while performing a rolling upgrade. Each upgraded node is joining an existing cluster so there is -no need for <>. +no need for <>. You +must configure <> on every node. -- . *Upgrade any plugins.* From 72f75393da80270015ad49e434964700a82a83ac Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Mon, 2 Dec 2019 16:03:52 +0200 Subject: [PATCH 034/686] Fix Locate function optional parameter handling (#49666) --- .../qa/src/main/resources/functions.csv-spec | 20 +++++++++++++++++++ .../main/resources/string-functions.sql-spec | 3 +++ .../function/scalar/string/Locate.java | 6 ++++-- .../string/LocateFunctionPipeTests.java | 11 +++++----- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/sql/qa/src/main/resources/functions.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/functions.csv-spec index 97edeb08ecb32..2fa54c18547fc 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/functions.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/functions.csv-spec @@ -333,6 +333,26 @@ SELECT LOCATE('a',"first_name") pos, INSERT("first_name",LOCATE('a',"first_name" 8 |ChirstiAAAn ; +selectLocateWithConditional1 +SELECT CAST(LOCATE('a', CASE WHEN TRUNCATE(salary, 3) > 40000 THEN first_name.keyword ELSE last_name.keyword END) > 0 AS STRING) AS x, COUNT(*) AS c FROM test_emp GROUP BY x ORDER BY c ASC; + + x:s | c:l +---------------+--------------- +null |4 +false |43 +true |53 +; + +selectLocateWithConditional2 +SELECT CAST(LOCATE(CASE WHEN languages > 3 THEN 'a' ELSE 'b' END, CASE WHEN TRUNCATE(salary, 3) > 40000 THEN first_name.keyword ELSE last_name.keyword END, CASE WHEN gender IS NOT NULL THEN 3 ELSE NULL END) > 0 AS STRING) AS x, COUNT(*) AS c FROM test_emp GROUP BY x ORDER BY c DESC; + + x:s | c:l +---------------+--------------- +false |80 +true |16 +null |4 +; + selectLeft SELECT LEFT("first_name",2) f FROM "test_emp" ORDER BY "first_name" LIMIT 10; diff --git a/x-pack/plugin/sql/qa/src/main/resources/string-functions.sql-spec b/x-pack/plugin/sql/qa/src/main/resources/string-functions.sql-spec index b82d6ef580799..313956cfcb932 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/string-functions.sql-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/string-functions.sql-spec @@ -253,6 +253,9 @@ SELECT LOCATE('a',"first_name",7) pos, INSERT("first_name",LOCATE('a',"first_nam selectLocateAndInsertWithLocateWithConditionAndTwoParameters SELECT LOCATE('a',"first_name") pos, INSERT("first_name",LOCATE('a',"first_name"),1,'AAA') inserted FROM "test_emp" WHERE LOCATE('a',"first_name") > 0 ORDER BY "first_name" LIMIT 10; +selectLocateWithConditional +SELECT LOCATE(CASE WHEN FALSE THEN NULL ELSE 'x' END, "first_name") > 0 AS x, COUNT(*) AS c FROM "test_emp" GROUP BY x ORDER BY c ASC; + selectLeft SELECT LEFT("first_name",2) f FROM "test_emp" ORDER BY "first_name" NULLS LAST LIMIT 10; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Locate.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Locate.java index 806e6fab8e465..9d7f43a3e6c0b 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Locate.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Locate.java @@ -133,10 +133,12 @@ public DataType dataType() { @Override public Expression replaceChildren(List newChildren) { - if (newChildren.size() != 3) { + if (start != null && newChildren.size() != 3) { throw new IllegalArgumentException("expected [3] children but received [" + newChildren.size() + "]"); + } else if (start == null && newChildren.size() != 2) { + throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]"); } - return new Locate(source(), newChildren.get(0), newChildren.get(1), newChildren.get(2)); + return new Locate(source(), newChildren.get(0), newChildren.get(1), start == null ? null : newChildren.get(2)); } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionPipeTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionPipeTests.java index 95c196c732bd0..3795aab2cd18f 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionPipeTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionPipeTests.java @@ -75,24 +75,23 @@ public void testReplaceChildren() { LocateFunctionPipe b = randomInstance(); Pipe newPattern = pipe(((Expression) randomValueOtherThan(b.pattern(), () -> randomStringLiteral()))); Pipe newSource = pipe(((Expression) randomValueOtherThan(b.source(), () -> randomStringLiteral()))); - Pipe newStart; + Pipe newStart = b.start() == null ? null : pipe(((Expression) randomValueOtherThan(b.start(), () -> randomIntLiteral()))); - LocateFunctionPipe newB = new LocateFunctionPipe( - b.source(), b.expression(), b.pattern(), b.src(), b.start()); - newStart = pipe(((Expression) randomValueOtherThan(b.start(), () -> randomIntLiteral()))); + LocateFunctionPipe newB = new LocateFunctionPipe(b.source(), b.expression(), b.pattern(), b.src(), b.start()); LocateFunctionPipe transformed = null; // generate all the combinations of possible children modifications and test all of them for(int i = 1; i < 4; i++) { for(BitSet comb : new Combinations(3, i)) { + Pipe tempNewStart = b.start() == null ? b.start() : (comb.get(2) ? newStart : b.start()); transformed = (LocateFunctionPipe) newB.replaceChildren( comb.get(0) ? newPattern : b.pattern(), comb.get(1) ? newSource : b.src(), - comb.get(2) ? newStart : b.start()); + tempNewStart); assertEquals(transformed.pattern(), comb.get(0) ? newPattern : b.pattern()); assertEquals(transformed.src(), comb.get(1) ? newSource : b.src()); - assertEquals(transformed.start(), comb.get(2) ? newStart : b.start()); + assertEquals(transformed.start(), tempNewStart); assertEquals(transformed.expression(), b.expression()); assertEquals(transformed.source(), b.source()); } From 37c8487a84b7fcf7a00142270fdbb5ca82d3ce08 Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Mon, 2 Dec 2019 16:04:28 +0200 Subject: [PATCH 035/686] Fix NULL handling for FLOOR and CEIL math functions (#49644) --- .../sql/qa/src/main/resources/math.sql-spec | 6 ++++++ .../expression/function/scalar/math/Ceil.java | 6 +++++- .../expression/function/scalar/math/Floor.java | 6 +++++- .../math/MathFunctionProcessorTests.java | 18 ++++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/sql/qa/src/main/resources/math.sql-spec b/x-pack/plugin/sql/qa/src/main/resources/math.sql-spec index 96ffa773aabaa..c6adaf9523ad0 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/math.sql-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/math.sql-spec @@ -17,6 +17,8 @@ SELECT ATAN(emp_no) m, first_name FROM "test_emp" WHERE emp_no < 10010 ORDER BY mathCeil // H2 returns CEIL as a double despite the value being an integer; we return a long as the other DBs SELECT CAST(CEIL(emp_no) AS INT) m, first_name FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; +mathCeilWithNulls +SELECT CAST(CEIL(languages) AS INT) m FROM "test_emp" ORDER BY emp_no; mathCos SELECT COS(emp_no) m, first_name FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; mathCosh @@ -31,6 +33,8 @@ mathExpm1 SELECT EXP(emp_no) m, first_name FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; mathFloor SELECT CAST(FLOOR(emp_no) AS INT) m, first_name FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; +mathFloorWithNulls +SELECT CAST(FLOOR(languages) AS INT) m FROM "test_emp" ORDER BY emp_no; mathLog SELECT LOG(emp_no) m, first_name FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; mathLog10 @@ -49,6 +53,8 @@ mathSqrt SELECT SQRT(emp_no) m, first_name FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; mathTan SELECT TAN(emp_no) m, first_name FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; +mathFloorAndCeilWithNullLiteral +SELECT CAST(FLOOR(CAST(NULL AS DOUBLE)) AS INT) fnull, CAST(CEIL(CAST(NULL AS LONG)) AS INT) cnull, gender FROM "test_emp" ORDER BY emp_no; // // Combined methods diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Ceil.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Ceil.java index 556f53918d892..5c9438c677221 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Ceil.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Ceil.java @@ -33,7 +33,11 @@ protected Ceil replaceChild(Expression newChild) { @Override public Number fold() { - return DataTypeConversion.toInteger((double) super.fold(), dataType()); + Object result = super.fold(); + if (result == null) { + return null; + } + return DataTypeConversion.toInteger((double) result, dataType()); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Floor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Floor.java index 03d6606b0e9fd..a77a4e497d31a 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Floor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Floor.java @@ -33,7 +33,11 @@ protected Floor replaceChild(Expression newChild) { @Override public Object fold() { - return DataTypeConversion.toInteger((double) super.fold(), dataType()); + Object result = super.fold(); + if (result == null) { + return null; + } + return DataTypeConversion.toInteger((double) result, dataType()); } @Override diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/MathFunctionProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/MathFunctionProcessorTests.java index 9ff32c5a05741..579ca5f056d63 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/MathFunctionProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/MathFunctionProcessorTests.java @@ -55,4 +55,22 @@ public void testRandom() { assertNotNull(proc.process(null)); assertNotNull(proc.process(randomLong())); } + + public void testFloor() { + MathProcessor proc = new MathProcessor(MathOperation.FLOOR); + assertNull(proc.process(null)); + assertNotNull(proc.process(randomLong())); + assertEquals(3.0, proc.process(3.3)); + assertEquals(3.0, proc.process(3.9)); + assertEquals(-13.0, proc.process(-12.1)); + } + + public void testCeil() { + MathProcessor proc = new MathProcessor(MathOperation.CEIL); + assertNull(proc.process(null)); + assertNotNull(proc.process(randomLong())); + assertEquals(4.0, proc.process(3.3)); + assertEquals(4.0, proc.process(3.9)); + assertEquals(-12.0, proc.process(-12.1)); + } } From 5c216357f78cb4c7d2a71eb7edab3a44c63713ec Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Mon, 2 Dec 2019 16:05:05 +0200 Subject: [PATCH 036/686] SQL: handle NULL arithmetic operations with INTERVALs (#49633) --- .../sql/functions/date-time.asciidoc | 2 +- .../qa/src/main/resources/arithmetic.csv-spec | 9 +++++ .../main/resources/datetime-interval.csv-spec | 9 +++++ .../xpack/sql/expression/TypeResolutions.java | 7 ++-- .../sql/expression/function/scalar/Cast.java | 3 +- .../predicate/conditional/Case.java | 2 +- .../expression/predicate/nulls/IsNotNull.java | 5 +-- .../expression/predicate/nulls/IsNull.java | 5 +-- .../DateTimeArithmeticOperation.java | 2 +- .../predicate/operator/arithmetic/Mul.java | 6 +-- .../xpack/sql/querydsl/query/TermsQuery.java | 3 +- .../xpack/sql/type/DataType.java | 12 ++++++ .../xpack/sql/type/DataTypeConversion.java | 4 +- .../xpack/sql/type/DataTypes.java | 4 -- .../arithmetic/BinaryArithmeticTests.java | 39 +++++++++++++++++++ 15 files changed, 86 insertions(+), 26 deletions(-) diff --git a/docs/reference/sql/functions/date-time.asciidoc b/docs/reference/sql/functions/date-time.asciidoc index c3158244ec265..74a58da8fa358 100644 --- a/docs/reference/sql/functions/date-time.asciidoc +++ b/docs/reference/sql/functions/date-time.asciidoc @@ -55,7 +55,7 @@ s|Description ==== Operators -Basic arithmetic operators (`+`, `-`, etc) support date/time parameters as indicated below: +Basic arithmetic operators (`+`, `-`, `*`) support date/time parameters as indicated below: [source, sql] -------------------------------------------------- diff --git a/x-pack/plugin/sql/qa/src/main/resources/arithmetic.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/arithmetic.csv-spec index 9cbf544fd8d56..31b2417932aa4 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/arithmetic.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/arithmetic.csv-spec @@ -18,3 +18,12 @@ SELECT 5 - 2 x FROM test_emp LIMIT 5; 3 ; + +nullArithmetics +schema::a:i|b:d|c:s|d:s|e:l|f:i|g:i|h:i|i:i|j:i|k:d +SELECT null + 2 AS a, null * 1.5 AS b, null + null AS c, null - null AS d, null - 1234567890123 AS e, 123 - null AS f, null / 5 AS g, 5 / null AS h, null % 5 AS i, 5 % null AS j, null + 5.5 - (null * (null * 3)) AS k; + + a | b | c | d | e | f | g | h | i | j | k +---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+--------------- +null |null |null |null |null |null |null |null |null |null |null +; diff --git a/x-pack/plugin/sql/qa/src/main/resources/datetime-interval.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/datetime-interval.csv-spec index 9bb89408923b6..785ea8f404dce 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/datetime-interval.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/datetime-interval.csv-spec @@ -191,6 +191,15 @@ SELECT 4 * -INTERVAL '2' HOURS AS result1, -5 * -INTERVAL '3' HOURS AS result2; -0 08:00:00.0 | +0 15:00:00.0 ; +intervalNullMath +schema::null_multiply:string|null_sub1:string|null_sub2:string|null_add:string +SELECT null * INTERVAL '1 23:45' DAY TO MINUTES AS null_multiply, INTERVAL '1' DAY - null AS null_sub1, null - INTERVAL '1' DAY AS null_sub2, INTERVAL 1 DAY + null AS null_add; + + null_multiply | null_sub1 | null_sub2 | null_add +-----------------+-------------+-------------+------------- +null |null |null |null +; + intervalAndFieldMultiply schema::languages:byte|result:string SELECT languages, CAST (languages * INTERVAL '1 10:30' DAY TO MINUTES AS string) AS result FROM test_emp ORDER BY emp_no LIMIT 5; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/TypeResolutions.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/TypeResolutions.java index c465ab1b2deb8..30041ea12224b 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/TypeResolutions.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/TypeResolutions.java @@ -5,8 +5,9 @@ */ package org.elasticsearch.xpack.sql.expression; +import org.elasticsearch.xpack.sql.expression.Expression.TypeResolution; +import org.elasticsearch.xpack.sql.expression.Expressions.ParamOrdinal; import org.elasticsearch.xpack.sql.type.DataType; -import org.elasticsearch.xpack.sql.type.DataTypes; import org.elasticsearch.xpack.sql.type.EsField; import java.util.Locale; @@ -14,8 +15,6 @@ import java.util.function.Predicate; import static org.elasticsearch.common.logging.LoggerMessageFormat.format; -import static org.elasticsearch.xpack.sql.expression.Expression.TypeResolution; -import static org.elasticsearch.xpack.sql.expression.Expressions.ParamOrdinal; import static org.elasticsearch.xpack.sql.expression.Expressions.name; import static org.elasticsearch.xpack.sql.type.DataType.BOOLEAN; @@ -119,7 +118,7 @@ public static TypeResolution isType(Expression e, String operationName, ParamOrdinal paramOrd, String... acceptedTypes) { - return predicate.test(e.dataType()) || DataTypes.isNull(e.dataType())? + return predicate.test(e.dataType()) || e.dataType().isNull() ? TypeResolution.TYPE_RESOLVED : new TypeResolution(format(null, "{}argument of [{}] must be [{}], found value [{}] type [{}]", paramOrd == null || paramOrd == ParamOrdinal.DEFAULT ? "" : paramOrd.name().toLowerCase(Locale.ROOT) + " ", diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java index fd82d2bb4db23..c3c0da0570903 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java @@ -13,7 +13,6 @@ import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.DataType; import org.elasticsearch.xpack.sql.type.DataTypeConversion; -import org.elasticsearch.xpack.sql.type.DataTypes; import java.util.Objects; @@ -64,7 +63,7 @@ public Object fold() { @Override public Nullability nullable() { - if (DataTypes.isNull(from())) { + if (from().isNull()) { return Nullability.TRUE; } return field().nullable(); diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/Case.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/Case.java index 1354bb27034ac..84f17283e061e 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/Case.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/Case.java @@ -78,7 +78,7 @@ protected NodeInfo info() { protected TypeResolution resolveType() { DataType expectedResultDataType = null; for (IfConditional ifConditional : conditions) { - if (DataTypes.isNull(ifConditional.result().dataType()) == false) { + if (ifConditional.result().dataType().isNull() == false) { expectedResultDataType = ifConditional.result().dataType(); break; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/IsNotNull.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/IsNotNull.java index f43e12e0b405c..53bf9bcc80504 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/IsNotNull.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/IsNotNull.java @@ -12,10 +12,9 @@ import org.elasticsearch.xpack.sql.expression.gen.script.Scripts; import org.elasticsearch.xpack.sql.expression.predicate.Negatable; import org.elasticsearch.xpack.sql.expression.predicate.nulls.CheckNullProcessor.CheckNullOperation; -import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.DataType; -import org.elasticsearch.xpack.sql.type.DataTypes; public class IsNotNull extends UnaryScalarFunction implements Negatable { @@ -35,7 +34,7 @@ protected IsNotNull replaceChild(Expression newChild) { @Override public Object fold() { - return field().fold() != null && !DataTypes.isNull(field().dataType()); + return field().fold() != null && !field().dataType().isNull(); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/IsNull.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/IsNull.java index b873f2770c724..c1d98dbe1b5c5 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/IsNull.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/IsNull.java @@ -12,10 +12,9 @@ import org.elasticsearch.xpack.sql.expression.gen.script.Scripts; import org.elasticsearch.xpack.sql.expression.predicate.Negatable; import org.elasticsearch.xpack.sql.expression.predicate.nulls.CheckNullProcessor.CheckNullOperation; -import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.DataType; -import org.elasticsearch.xpack.sql.type.DataTypes; public class IsNull extends UnaryScalarFunction implements Negatable { @@ -35,7 +34,7 @@ protected IsNull replaceChild(Expression newChild) { @Override public Object fold() { - return field().fold() == null || DataTypes.isNull(field().dataType()); + return field().fold() == null || field().dataType().isNull(); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java index b7b559f1b8617..39797a7351627 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java @@ -56,7 +56,7 @@ protected TypeResolution resolveWithIntervals() { DataType l = left().dataType(); DataType r = right().dataType(); - if (!(r.isDateOrTimeBased() || r.isInterval())|| !(l.isDateOrTimeBased() || l.isInterval())) { + if (!(r.isDateOrTimeBased() || r.isInterval() || r.isNull())|| !(l.isDateOrTimeBased() || l.isInterval() || l.isNull())) { return new TypeResolution(format(null, "[{}] has arguments with incompatible types [{}] and [{}]", symbol(), l, r)); } return TypeResolution.TYPE_RESOLVED; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/Mul.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/Mul.java index 9c12a24687612..f1e90c2dbd668 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/Mul.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/Mul.java @@ -34,14 +34,14 @@ protected TypeResolution resolveType() { DataType r = right().dataType(); // 1. both are numbers - if (l.isNumeric() && r.isNumeric()) { + if (l.isNullOrNumeric() && r.isNullOrNumeric()) { return TypeResolution.TYPE_RESOLVED; } - if (l.isInterval() && r.isInteger()) { + if (l.isNullOrInterval() && (r.isInteger() || r.isNull())) { dataType = l; return TypeResolution.TYPE_RESOLVED; - } else if (r.isInterval() && l.isInteger()) { + } else if (r.isNullOrInterval() && (l.isInteger() || l.isNull())) { dataType = r; return TypeResolution.TYPE_RESOLVED; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/query/TermsQuery.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/query/TermsQuery.java index 966130cb239ec..9b7f59c011ed5 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/query/TermsQuery.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/query/TermsQuery.java @@ -9,7 +9,6 @@ import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.Foldables; import org.elasticsearch.xpack.sql.tree.Source; -import org.elasticsearch.xpack.sql.type.DataTypes; import java.util.Collections; import java.util.LinkedHashSet; @@ -27,7 +26,7 @@ public class TermsQuery extends LeafQuery { public TermsQuery(Source source, String term, List values) { super(source); this.term = term; - values.removeIf(e -> DataTypes.isNull(e.dataType())); + values.removeIf(e -> e.dataType().isNull()); if (values.isEmpty()) { this.values = Collections.emptySet(); } else { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataType.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataType.java index 22f2a596e3515..19fda28f76ffd 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataType.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataType.java @@ -247,6 +247,18 @@ public boolean isSigned() { return isNumeric(); } + public boolean isNull() { + return this == NULL; + } + + public boolean isNullOrNumeric() { + return isNull() || isNumeric(); + } + + public boolean isNullOrInterval() { + return isNull() || isInterval(); + } + public boolean isString() { return this == KEYWORD || this == TEXT; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataTypeConversion.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataTypeConversion.java index a9b836da1354d..8e3158c3e6a8a 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataTypeConversion.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataTypeConversion.java @@ -45,10 +45,10 @@ public static DataType commonType(DataType left, DataType right) { if (left == right) { return left; } - if (DataTypes.isNull(left)) { + if (left.isNull()) { return right; } - if (DataTypes.isNull(right)) { + if (right.isNull()) { return left; } if (left.isString() && right.isString()) { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataTypes.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataTypes.java index 5d4691e70d531..1faf19e1d0996 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataTypes.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataTypes.java @@ -31,10 +31,6 @@ public final class DataTypes { private DataTypes() {} - public static boolean isNull(DataType from) { - return from == NULL; - } - public static boolean isUnsupported(DataType from) { return from == UNSUPPORTED; } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/BinaryArithmeticTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/BinaryArithmeticTests.java index 1c4b0697f959e..1b877cb75f377 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/BinaryArithmeticTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/operator/arithmetic/BinaryArithmeticTests.java @@ -227,6 +227,45 @@ public void testMulNumberInterval() { Period p = interval.interval(); assertEquals(Period.ofYears(2).negated(), p); } + + public void testMulNullInterval() { + Literal literal = interval(Period.ofMonths(1), INTERVAL_MONTH); + Mul result = new Mul(EMPTY, L(null), literal); + assertTrue(result.foldable()); + assertNull(result.fold()); + assertEquals(INTERVAL_MONTH, result.dataType()); + + result = new Mul(EMPTY, literal, L(null)); + assertTrue(result.foldable()); + assertNull(result.fold()); + assertEquals(INTERVAL_MONTH, result.dataType()); + } + + public void testAddNullInterval() { + Literal literal = interval(Period.ofMonths(1), INTERVAL_MONTH); + Add result = new Add(EMPTY, L(null), literal); + assertTrue(result.foldable()); + assertNull(result.fold()); + assertEquals(INTERVAL_MONTH, result.dataType()); + + result = new Add(EMPTY, literal, L(null)); + assertTrue(result.foldable()); + assertNull(result.fold()); + assertEquals(INTERVAL_MONTH, result.dataType()); + } + + public void testSubNullInterval() { + Literal literal = interval(Period.ofMonths(1), INTERVAL_MONTH); + Sub result = new Sub(EMPTY, L(null), literal); + assertTrue(result.foldable()); + assertNull(result.fold()); + assertEquals(INTERVAL_MONTH, result.dataType()); + + result = new Sub(EMPTY, literal, L(null)); + assertTrue(result.foldable()); + assertNull(result.fold()); + assertEquals(INTERVAL_MONTH, result.dataType()); + } @SuppressWarnings("unchecked") private static T add(Object l, Object r) { From f01004f4576a1e030d36e80ee0aa60719e1ab537 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 2 Dec 2019 09:12:21 -0500 Subject: [PATCH 037/686] [DOCS] Explicitly document enrich `target_field` includes `match_field` (#49407) When the enrich processor appends enrich data to an incoming document, it adds a `target_field` to contain the enrich data. This `target_field` contains both the `match_field` AND `enrich_fields` specified in the enrich policy. Previously, this was reflected in the documented example but not explicitly stated. This adds several explicit statements to the docs. --- docs/reference/ingest/enrich.asciidoc | 6 +++++- docs/reference/ingest/processors/enrich.asciidoc | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/reference/ingest/enrich.asciidoc b/docs/reference/ingest/enrich.asciidoc index fbb1276e4bcaa..5a8e0dab3a25a 100644 --- a/docs/reference/ingest/enrich.asciidoc +++ b/docs/reference/ingest/enrich.asciidoc @@ -180,7 +180,7 @@ When defining the enrich processor, you must include at least the following: * The enrich policy to use. * The field used to match incoming documents to the documents in your enrich index. * The target field to add to incoming documents. This target field contains the -enrich fields specified in your enrich policy. +match and enrich fields specified in your enrich policy. You also can use the `max_matches` option to set the number of enrich documents an incoming document can match. If set to the default of `1`, data is added to @@ -371,6 +371,8 @@ the pipeline, add an <> that includes: * The `field` of incoming documents used to match the geo_shape of documents from the enrich index. * The `target_field` used to store appended enrich data for incoming documents. + This field contains the `match_field` and `enrich_fields` specified in your + enrich policy. * The `shape_relation`, which indicates how the processor matches geo_shapes in incoming documents to geo_shapes in documents from the enrich index. See <<_spatial_relations>> for valid options and more information. @@ -526,6 +528,8 @@ the pipeline, add an <> that includes: * The `field` of incoming documents used to match documents from the enrich index. * The `target_field` used to store appended enrich data for incoming documents. + This field contains the `match_field` and `enrich_fields` specified in your + enrich policy. [source,console] ---- diff --git a/docs/reference/ingest/processors/enrich.asciidoc b/docs/reference/ingest/processors/enrich.asciidoc index f9766261f4b49..fee2cf7974001 100644 --- a/docs/reference/ingest/processors/enrich.asciidoc +++ b/docs/reference/ingest/processors/enrich.asciidoc @@ -13,7 +13,7 @@ See <> section for more information about how | Name | Required | Default | Description | `policy_name` | yes | - | The name of the enrich policy to use. | `field` | yes | - | The field in the input document that matches the policies match_field used to retrieve the enrichment data. Supports <>. -| `target_field` | yes | - | The field that will be used for the enrichment data. Supports <>. +| `target_field` | yes | - | Field added to incoming documents to contain enrich data. This field contains both the `match_field` and `enrich_fields` specified in the <>. Supports <>. | `ignore_missing` | no | false | If `true` and `field` does not exist, the processor quietly exits without modifying the document | `override` | no | true | If processor will update fields with pre-existing non-null-valued field. When set to `false`, such fields will not be touched. | `max_matches` | no | 1 | The maximum number of matched documents to include under the configured target field. The `target_field` will be turned into a json array if `max_matches` is higher than 1, otherwise `target_field` will become a json object. In order to avoid documents getting too large, the maximum allowed value is 128. From a05bc15db783b5bc6a7da0fc51b8b674d0eed14d Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Mon, 2 Dec 2019 15:17:18 +0100 Subject: [PATCH 038/686] [Transform] Fix possible audit logging disappearance after rolling upgrade (#49731) ensure audit index template is available during a rolling upgrade before a transform task can write to it. fixes #49730 --- .../TransformInternalIndexConstants.java | 3 +- .../persistence/TransformInternalIndex.java | 78 ++++++++++-- .../TransformPersistentTasksExecutor.java | 4 +- .../TransformInternalIndexTests.java | 112 +++++++++++++++++- .../80_transform_jobs_crud.yml | 8 +- 5 files changed, 188 insertions(+), 17 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/persistence/TransformInternalIndexConstants.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/persistence/TransformInternalIndexConstants.java index 574499397e9fd..474943f0e8c26 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/persistence/TransformInternalIndexConstants.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/persistence/TransformInternalIndexConstants.java @@ -32,7 +32,8 @@ public final class TransformInternalIndexConstants { public static final String INDEX_NAME_PATTERN_DEPRECATED = ".data-frame-internal-*"; // audit index - public static final String AUDIT_TEMPLATE_VERSION = "000001"; + // gh #49730: upped version of audit index to 000002 + public static final String AUDIT_TEMPLATE_VERSION = "000002"; public static final String AUDIT_INDEX_PREFIX = ".transform-notifications-"; public static final String AUDIT_INDEX_PATTERN = AUDIT_INDEX_PREFIX + "*"; public static final String AUDIT_INDEX_DEPRECATED = ".data-frame-notifications-1"; diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndex.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndex.java index bd436bde9aa2d..d0b94bf6e3e52 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndex.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndex.java @@ -315,22 +315,39 @@ private static XContentBuilder addMetaInformation(XContentBuilder builder) throw .endObject(); } - public static boolean haveLatestVersionedIndexTemplate(ClusterState state) { - return state.getMetaData().getTemplates().containsKey(TransformInternalIndexConstants.LATEST_INDEX_VERSIONED_NAME); - } - /** * This method should be called before any document is indexed that relies on the - * existence of the latest index template to create the internal index. The - * reason is that the standard template upgrader only runs when the master node + * existence of the latest index templates to create the internal and audit index. + * The reason is that the standard template upgrader only runs when the master node * is upgraded to the newer version. If data nodes are upgraded before master * nodes and transforms get assigned to those data nodes then without this check * the data nodes will index documents into the internal index before the necessary * index template is present and this will result in an index with completely * dynamic mappings being created (which is very bad). */ - public static void installLatestVersionedIndexTemplateIfRequired(ClusterService clusterService, Client client, - ActionListener listener) { + public static void installLatestIndexTemplatesIfRequired(ClusterService clusterService, Client client, ActionListener listener) { + + installLatestVersionedIndexTemplateIfRequired( + clusterService, + client, + ActionListener.wrap(r -> { installLatestAuditIndexTemplateIfRequired(clusterService, client, listener); }, listener::onFailure) + ); + + } + + protected static boolean haveLatestVersionedIndexTemplate(ClusterState state) { + return state.getMetaData().getTemplates().containsKey(TransformInternalIndexConstants.LATEST_INDEX_VERSIONED_NAME); + } + + protected static boolean haveLatestAuditIndexTemplate(ClusterState state) { + return state.getMetaData().getTemplates().containsKey(TransformInternalIndexConstants.AUDIT_INDEX); + } + + protected static void installLatestVersionedIndexTemplateIfRequired( + ClusterService clusterService, + Client client, + ActionListener listener + ) { // The check for existence of the template is against local cluster state, so very cheap if (haveLatestVersionedIndexTemplate(clusterService.state())) { @@ -348,13 +365,52 @@ public static void installLatestVersionedIndexTemplateIfRequired(ClusterService .settings(indexTemplateMetaData.settings()) .mapping(SINGLE_MAPPING_NAME, XContentHelper.convertToMap(jsonMappings, true, XContentType.JSON).v2()); ActionListener innerListener = ActionListener.wrap(r -> listener.onResponse(null), listener::onFailure); - executeAsyncWithOrigin(client.threadPool().getThreadContext(), TRANSFORM_ORIGIN, request, - innerListener, client.admin().indices()::putTemplate); + executeAsyncWithOrigin( + client.threadPool().getThreadContext(), + TRANSFORM_ORIGIN, + request, + innerListener, + client.admin().indices()::putTemplate + ); } catch (IOException e) { listener.onFailure(e); } } - private TransformInternalIndex() { + protected static void installLatestAuditIndexTemplateIfRequired( + ClusterService clusterService, + Client client, + ActionListener listener + ) { + + // The check for existence of the template is against local cluster state, so very cheap + if (haveLatestAuditIndexTemplate(clusterService.state())) { + listener.onResponse(null); + return; + } + + // Installing the template involves communication with the master node, so it's more expensive but much rarer + try { + IndexTemplateMetaData indexTemplateMetaData = getAuditIndexTemplateMetaData(); + BytesReference jsonMappings = new BytesArray(indexTemplateMetaData.mappings().get(SINGLE_MAPPING_NAME).uncompressed()); + PutIndexTemplateRequest request = new PutIndexTemplateRequest(TransformInternalIndexConstants.AUDIT_INDEX).patterns( + indexTemplateMetaData.patterns() + ) + .version(indexTemplateMetaData.version()) + .settings(indexTemplateMetaData.settings()) + .mapping(SINGLE_MAPPING_NAME, XContentHelper.convertToMap(jsonMappings, true, XContentType.JSON).v2()); + ActionListener innerListener = ActionListener.wrap(r -> listener.onResponse(null), listener::onFailure); + executeAsyncWithOrigin( + client.threadPool().getThreadContext(), + TRANSFORM_ORIGIN, + request, + innerListener, + client.admin().indices()::putTemplate + ); + } catch (IOException e) { + listener.onFailure(e); + } } + + private TransformInternalIndex() {} } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutor.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutor.java index 3c3c7c7e1a3f0..2ed273128c6bd 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutor.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutor.java @@ -271,8 +271,8 @@ protected void nodeOperation(AllocatedPersistentTask task, @Nullable TransformTa } ); - // <1> Check the internal index template is installed - TransformInternalIndex.installLatestVersionedIndexTemplateIfRequired(clusterService, client, templateCheckListener); + // <1> Check the index templates are installed + TransformInternalIndex.installLatestIndexTemplatesIfRequired(clusterService, client, templateCheckListener); } private static IndexerState currentIndexerState(TransformState previousState) { diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndexTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndexTests.java index 83f9b36c496ab..b49546d8f11cc 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndexTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndexTests.java @@ -38,6 +38,7 @@ public class TransformInternalIndexTests extends ESTestCase { public static ClusterState STATE_WITH_LATEST_VERSIONED_INDEX_TEMPLATE; + public static ClusterState STATE_WITH_LATEST_AUDIT_INDEX_TEMPLATE; static { ImmutableOpenMap.Builder mapBuilder = ImmutableOpenMap.builder(); @@ -51,6 +52,18 @@ public class TransformInternalIndexTests extends ESTestCase { ClusterState.Builder csBuilder = ClusterState.builder(ClusterName.DEFAULT); csBuilder.metaData(metaBuilder.build()); STATE_WITH_LATEST_VERSIONED_INDEX_TEMPLATE = csBuilder.build(); + + mapBuilder = ImmutableOpenMap.builder(); + try { + mapBuilder.put(TransformInternalIndexConstants.AUDIT_INDEX, TransformInternalIndex.getAuditIndexTemplateMetaData()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + metaBuilder = MetaData.builder(); + metaBuilder.templates(mapBuilder.build()); + csBuilder = ClusterState.builder(ClusterName.DEFAULT); + csBuilder.metaData(metaBuilder.build()); + STATE_WITH_LATEST_AUDIT_INDEX_TEMPLATE = csBuilder.build(); } public void testHaveLatestVersionedIndexTemplate() { @@ -81,8 +94,7 @@ public void testInstallLatestVersionedIndexTemplateIfRequired_GivenRequired() { when(clusterService.state()).thenReturn(ClusterState.EMPTY_STATE); IndicesAdminClient indicesClient = mock(IndicesAdminClient.class); - doAnswer( - invocationOnMock -> { + doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; listener.onResponse(new AcknowledgedResponse(true)); @@ -112,4 +124,100 @@ public void testInstallLatestVersionedIndexTemplateIfRequired_GivenRequired() { verify(indicesClient, times(1)).putTemplate(any(), any()); verifyNoMoreInteractions(indicesClient); } + + public void testHaveLatestAuditIndexTemplate() { + + assertTrue(TransformInternalIndex.haveLatestAuditIndexTemplate(STATE_WITH_LATEST_AUDIT_INDEX_TEMPLATE)); + assertFalse(TransformInternalIndex.haveLatestAuditIndexTemplate(ClusterState.EMPTY_STATE)); + } + + public void testInstallLatestAuditIndexTemplateIfRequired_GivenNotRequired() { + + ClusterService clusterService = mock(ClusterService.class); + when(clusterService.state()).thenReturn(TransformInternalIndexTests.STATE_WITH_LATEST_AUDIT_INDEX_TEMPLATE); + + Client client = mock(Client.class); + + AtomicBoolean gotResponse = new AtomicBoolean(false); + ActionListener testListener = ActionListener.wrap(aVoid -> gotResponse.set(true), e -> fail(e.getMessage())); + + TransformInternalIndex.installLatestAuditIndexTemplateIfRequired(clusterService, client, testListener); + + assertTrue(gotResponse.get()); + verifyNoMoreInteractions(client); + } + + public void testInstallLatestAuditIndexTemplateIfRequired_GivenRequired() { + + ClusterService clusterService = mock(ClusterService.class); + when(clusterService.state()).thenReturn(ClusterState.EMPTY_STATE); + + IndicesAdminClient indicesClient = mock(IndicesAdminClient.class); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + listener.onResponse(new AcknowledgedResponse(true)); + return null; + }).when(indicesClient).putTemplate(any(), any()); + + AdminClient adminClient = mock(AdminClient.class); + when(adminClient.indices()).thenReturn(indicesClient); + Client client = mock(Client.class); + when(client.admin()).thenReturn(adminClient); + + ThreadPool threadPool = mock(ThreadPool.class); + when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); + when(client.threadPool()).thenReturn(threadPool); + + AtomicBoolean gotResponse = new AtomicBoolean(false); + ActionListener testListener = ActionListener.wrap(aVoid -> gotResponse.set(true), e -> fail(e.getMessage())); + + TransformInternalIndex.installLatestAuditIndexTemplateIfRequired(clusterService, client, testListener); + + assertTrue(gotResponse.get()); + verify(client, times(1)).threadPool(); + verify(client, times(1)).admin(); + verifyNoMoreInteractions(client); + verify(adminClient, times(1)).indices(); + verifyNoMoreInteractions(adminClient); + verify(indicesClient, times(1)).putTemplate(any(), any()); + verifyNoMoreInteractions(indicesClient); + } + + public void testInstallLatestIndexTemplateIfRequired_GivenRequired() { + + ClusterService clusterService = mock(ClusterService.class); + when(clusterService.state()).thenReturn(ClusterState.EMPTY_STATE); + + IndicesAdminClient indicesClient = mock(IndicesAdminClient.class); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + listener.onResponse(new AcknowledgedResponse(true)); + return null; + }).when(indicesClient).putTemplate(any(), any()); + + AdminClient adminClient = mock(AdminClient.class); + when(adminClient.indices()).thenReturn(indicesClient); + Client client = mock(Client.class); + when(client.admin()).thenReturn(adminClient); + + ThreadPool threadPool = mock(ThreadPool.class); + when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); + when(client.threadPool()).thenReturn(threadPool); + + AtomicBoolean gotResponse = new AtomicBoolean(false); + ActionListener testListener = ActionListener.wrap(aVoid -> gotResponse.set(true), e -> fail(e.getMessage())); + + TransformInternalIndex.installLatestIndexTemplatesIfRequired(clusterService, client, testListener); + + assertTrue(gotResponse.get()); + verify(client, times(2)).threadPool(); + verify(client, times(2)).admin(); + verifyNoMoreInteractions(client); + verify(adminClient, times(2)).indices(); + verifyNoMoreInteractions(adminClient); + verify(indicesClient, times(2)).putTemplate(any(), any()); + verifyNoMoreInteractions(indicesClient); + } } diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/80_transform_jobs_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/80_transform_jobs_crud.yml index 9aeaf6cdf82ca..d220c0b1a51ab 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/80_transform_jobs_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/80_transform_jobs_crud.yml @@ -261,7 +261,7 @@ setup: transform_id: "mixed-simple-continuous-transform" --- -"Test index mappings for latest internal index": +"Test index mappings for latest internal index and audit index": - do: transform.put_transform: transform_id: "upgraded-simple-transform" @@ -282,3 +282,9 @@ setup: index: .transform-internal-004 - match: { \.transform-internal-004.mappings.dynamic: "false" } - match: { \.transform-internal-004.mappings.properties.id.type: "keyword" } + - do: + indices.get_mapping: + index: .transform-notifications-000002 + - match: { \.transform-notifications-000002.mappings.dynamic: "false" } + - match: { \.transform-notifications-000002.mappings.properties.transform_id.type: "keyword" } + - match: { \.transform-notifications-000002.mappings.properties.timestamp.type: "date" } From 2b7cea85d0e235b17c471dbb4cf02fe7feff12ac Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 2 Dec 2019 09:22:21 -0500 Subject: [PATCH 039/686] [DOCS] Reformat keep types and keep words token filter docs (#49604) * Adds title abbreviations * Updates the descriptions and adds Lucene links * Reformats parameter definitions * Adds analyze and custom analyzer snippets * Adds explanations of token types to keep types token filter and tokenizer docs --- .../keep-types-tokenfilter.asciidoc | 225 +++++++++++------- .../keep-words-tokenfilter.asciidoc | 166 ++++++++++--- docs/reference/analysis/tokenizers.asciidoc | 11 +- 3 files changed, 283 insertions(+), 119 deletions(-) diff --git a/docs/reference/analysis/tokenfilters/keep-types-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/keep-types-tokenfilter.asciidoc index 39b32d15c991f..838c1252835ce 100644 --- a/docs/reference/analysis/tokenfilters/keep-types-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/keep-types-tokenfilter.asciidoc @@ -1,137 +1,202 @@ [[analysis-keep-types-tokenfilter]] -=== Keep Types Token Filter +=== Keep types token filter +++++ +Keep types +++++ -A token filter of type `keep_types` that only keeps tokens with a token type -contained in a predefined set. +Keeps or removes tokens of a specific type. For example, you can use this filter +to change `3 quick foxes` to `quick foxes` by keeping only `` +(alphanumeric) tokens. +[NOTE] +.Token types +==== +Token types are set by the <> when converting +characters to tokens. Token types can vary between tokenizers. -[float] -=== Options -[horizontal] -types:: a list of types to include (default mode) or exclude -mode:: if set to `include` (default) the specified token types will be kept, -if set to `exclude` the specified token types will be removed from the stream +For example, the <> tokenizer can +produce a variety of token types, including ``, ``, and +``. Simpler analyzers, like the +<> tokenizer, only produce the `word` +token type. -[float] -=== Settings example +Certain token filters can also add token types. For example, the +<> filter can add the `` token +type. +==== -You can set it up like: +This filter uses Lucene's +https://lucene.apache.org/core/{lucene_version_path}/analyzers-common/org/apache/lucene/analysis/core/TypeTokenFilter.html[TypeTokenFilter]. + +[[analysis-keep-types-tokenfilter-analyze-include-ex]] +==== Include example + +The following <> request uses the `keep_types` +filter to keep only `` (numeric) tokens from `1 quick fox 2 lazy dogs`. [source,console] -------------------------------------------------- -PUT /keep_types_example +GET _analyze { - "settings" : { - "analysis" : { - "analyzer" : { - "my_analyzer" : { - "tokenizer" : "standard", - "filter" : ["lowercase", "extract_numbers"] - } - }, - "filter" : { - "extract_numbers" : { - "type" : "keep_types", - "types" : [ "" ] - } - } - } + "tokenizer": "standard", + "filter": [ + { + "type": "keep_types", + "types": [ "" ] } + ], + "text": "1 quick fox 2 lazy dogs" } -------------------------------------------------- -And test it like: +The filter produces the following tokens: -[source,console] +[source,text] -------------------------------------------------- -POST /keep_types_example/_analyze -{ - "analyzer" : "my_analyzer", - "text" : "this is just 1 a test" -} +[ 1, 2 ] -------------------------------------------------- -// TEST[continued] - -The response will be: +///////////////////// [source,console-result] -------------------------------------------------- { "tokens": [ { "token": "1", - "start_offset": 13, - "end_offset": 14, + "start_offset": 0, + "end_offset": 1, + "type": "", + "position": 0 + }, + { + "token": "2", + "start_offset": 12, + "end_offset": 13, "type": "", "position": 3 } ] } -------------------------------------------------- +///////////////////// -Note how only the `` token is in the output. - -[discrete] -=== Exclude mode settings example +[[analysis-keep-types-tokenfilter-analyze-exclude-ex]] +==== Exclude example -If the `mode` parameter is set to `exclude` like in the following example: +The following <> request uses the `keep_types` +filter to remove `` tokens from `1 quick fox 2 lazy dogs`. Note the `mode` +parameter is set to `exclude`. [source,console] -------------------------------------------------- -PUT /keep_types_exclude_example +GET _analyze { - "settings" : { - "analysis" : { - "analyzer" : { - "my_analyzer" : { - "tokenizer" : "standard", - "filter" : ["lowercase", "remove_numbers"] - } - }, - "filter" : { - "remove_numbers" : { - "type" : "keep_types", - "mode" : "exclude", - "types" : [ "" ] - } - } - } + "tokenizer": "standard", + "filter": [ + { + "type": "keep_types", + "types": [ "" ], + "mode": "exclude" } + ], + "text": "1 quick fox 2 lazy dogs" } -------------------------------------------------- -And we test it like: +The filter produces the following tokens: -[source,console] +[source,text] -------------------------------------------------- -POST /keep_types_exclude_example/_analyze -{ - "analyzer" : "my_analyzer", - "text" : "hello 101 world" -} +[ quick, fox, lazy, dogs ] -------------------------------------------------- -// TEST[continued] - -The response will be: +///////////////////// [source,console-result] -------------------------------------------------- { "tokens": [ { - "token": "hello", - "start_offset": 0, - "end_offset": 5, + "token": "quick", + "start_offset": 2, + "end_offset": 7, "type": "", - "position": 0 - }, + "position": 1 + }, { - "token": "world", - "start_offset": 10, - "end_offset": 15, + "token": "fox", + "start_offset": 8, + "end_offset": 11, "type": "", "position": 2 + }, + { + "token": "lazy", + "start_offset": 14, + "end_offset": 18, + "type": "", + "position": 4 + }, + { + "token": "dogs", + "start_offset": 19, + "end_offset": 23, + "type": "", + "position": 5 } ] } -------------------------------------------------- +///////////////////// + +[[analysis-keep-types-tokenfilter-configure-parms]] +==== Configurable parameters + +`types`:: +(Required, array of strings) +List of token types to keep or remove. + +`mode`:: +(Optional, string) +Indicates whether to keep or remove the specified token types. +Valid values are: + +`include`::: +(Default) Keep only the specified token types. + +`exclude`::: +Remove the specified token types. + +[[analysis-keep-types-tokenfilter-customize]] +==== Customize and add to an analyzer + +To customize the `keep_types` filter, duplicate it to create the basis +for a new custom token filter. You can modify the filter using its configurable +parameters. + +For example, the following <> request +uses a custom `keep_types` filter to configure a new +<>. The custom `keep_types` filter +keeps only `` (alphanumeric) tokens. + +[source,console] +-------------------------------------------------- +PUT keep_types_example +{ + "settings": { + "analysis": { + "analyzer": { + "my_analyzer": { + "tokenizer": "standard", + "filter": [ "extract_alpha" ] + } + }, + "filter": { + "extract_alpha": { + "type": "keep_types", + "types": [ "" ] + } + } + } + } +} +-------------------------------------------------- diff --git a/docs/reference/analysis/tokenfilters/keep-words-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/keep-words-tokenfilter.asciidoc index 8166c4c2d5bba..a7dc784c3b734 100644 --- a/docs/reference/analysis/tokenfilters/keep-words-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/keep-words-tokenfilter.asciidoc @@ -1,50 +1,146 @@ [[analysis-keep-words-tokenfilter]] -=== Keep Words Token Filter +=== Keep words token filter +++++ +Keep words +++++ -A token filter of type `keep` that only keeps tokens with text contained in a -predefined set of words. The set of words can be defined in the settings or -loaded from a text file containing one word per line. +Keeps only tokens contained in a specified word list. +This filter uses Lucene's +https://lucene.apache.org/core/{lucene_version_path}/analyzers-common/org/apache/lucene/analysis/miscellaneous/KeepWordFilter.html[KeepWordFilter]. -[float] -=== Options -[horizontal] -keep_words:: a list of words to keep -keep_words_path:: a path to a words file -keep_words_case:: a boolean indicating whether to lower case the words (defaults to `false`) +[NOTE] +==== +To remove a list of words from a token stream, use the +<> filter. +==== +[[analysis-keep-words-tokenfilter-analyze-ex]] +==== Example +The following <> request uses the `keep` filter to +keep only the `fox` and `dog` tokens from +`the quick fox jumps over the lazy dog`. -[float] -=== Settings example +[source,console] +-------------------------------------------------- +GET _analyze +{ + "tokenizer": "whitespace", + "filter": [ + { + "type": "keep", + "keep_words": [ "dog", "elephant", "fox" ] + } + ], + "text": "the quick fox jumps over the lazy dog" +} +-------------------------------------------------- + +The filter produces the following tokens: + +[source,text] +-------------------------------------------------- +[ fox, dog ] +-------------------------------------------------- + +///////////////////// +[source,console-result] +-------------------------------------------------- +{ + "tokens": [ + { + "token": "fox", + "start_offset": 10, + "end_offset": 13, + "type": "word", + "position": 2 + }, + { + "token": "dog", + "start_offset": 34, + "end_offset": 37, + "type": "word", + "position": 7 + } + ] +} +-------------------------------------------------- +///////////////////// + +[[analysis-keep-words-tokenfilter-configure-parms]] +==== Configurable parameters + +`keep_words`:: ++ +-- +(Required+++*+++, array of strings) +List of words to keep. Only tokens that match words in this list are included in +the output. + +Either this parameter or `keep_words_path` must be specified. +-- + +`keep_words_path`:: ++ +-- +(Required+++*+++, array of strings) +Path to a file that contains a list of words to keep. Only tokens that match +words in this list are included in the output. + +This path must be absolute or relative to the `config` location, and the file +must be UTF-8 encoded. Each word in the file must be separated by a line break. + +Either this parameter or `keep_words` must be specified. +-- + +`keep_words_case`:: +(Optional, boolean) +If `true`, lowercase all keep words. Defaults to `false`. + +[[analysis-keep-words-tokenfilter-customize]] +==== Customize and add to an analyzer + +To customize the `keep` filter, duplicate it to create the basis for a new +custom token filter. You can modify the filter using its configurable +parameters. + +For example, the following <> request +uses custom `keep` filters to configure two new +<>: + +* `standard_keep_word_array`, which uses a custom `keep` filter with an inline + array of keep words +* `standard_keep_word_file`, which uses a customer `keep` filter with a keep + words file [source,console] -------------------------------------------------- -PUT /keep_words_example +PUT keep_words_example { - "settings" : { - "analysis" : { - "analyzer" : { - "example_1" : { - "tokenizer" : "standard", - "filter" : ["lowercase", "words_till_three"] - }, - "example_2" : { - "tokenizer" : "standard", - "filter" : ["lowercase", "words_in_file"] - } - }, - "filter" : { - "words_till_three" : { - "type" : "keep", - "keep_words" : [ "one", "two", "three"] - }, - "words_in_file" : { - "type" : "keep", - "keep_words_path" : "analysis/example_word_list.txt" - } - } + "settings": { + "analysis": { + "analyzer": { + "standard_keep_word_array": { + "tokenizer": "standard", + "filter": [ "keep_word_array" ] + }, + "standard_keep_word_file": { + "tokenizer": "standard", + "filter": [ "keep_word_file" ] + } + }, + "filter": { + "keep_word_array": { + "type": "keep", + "keep_words": [ "one", "two", "three" ] + }, + "keep_word_file": { + "type": "keep", + "keep_words_path": "analysis/example_word_list.txt" } + } } + } } -------------------------------------------------- diff --git a/docs/reference/analysis/tokenizers.asciidoc b/docs/reference/analysis/tokenizers.asciidoc index 106f08cdb0cca..f4a8a13aabb0b 100644 --- a/docs/reference/analysis/tokenizers.asciidoc +++ b/docs/reference/analysis/tokenizers.asciidoc @@ -7,10 +7,13 @@ instance, a <> tokenizer breaks text into tokens whenever it sees any whitespace. It would convert the text `"Quick brown fox!"` into the terms `[Quick, brown, fox!]`. -The tokenizer is also responsible for recording the order or _position_ of -each term (used for phrase and word proximity queries) and the start and end -_character offsets_ of the original word which the term represents (used for -highlighting search snippets). +The tokenizer is also responsible for recording the following: + +* Order or _position_ of each term (used for phrase and word proximity queries) +* Start and end _character offsets_ of the original word which the term +represents (used for highlighting search snippets). +* _Token type_, a classification of each term produced, such as ``, +``, or ``. Simpler analyzers only produce the `word` token type. Elasticsearch has a number of built in tokenizers which can be used to build <>. From a8b6c2305902bb5500aeae5745386a700220158f Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Mon, 2 Dec 2019 15:35:33 +0100 Subject: [PATCH 040/686] Add CoreValuesSourceTypeTests for histogram (#49751) --- .../aggregations/support/CoreValuesSourceTypeTests.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/support/CoreValuesSourceTypeTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/support/CoreValuesSourceTypeTests.java index e7e39e7a43b61..5f57a8ee9c12e 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/support/CoreValuesSourceTypeTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/support/CoreValuesSourceTypeTests.java @@ -38,6 +38,7 @@ public void testValidOrdinals() { assertThat(CoreValuesSourceType.BYTES.ordinal(), equalTo(2)); assertThat(CoreValuesSourceType.GEOPOINT.ordinal(), equalTo(3)); assertThat(CoreValuesSourceType.RANGE.ordinal(), equalTo(4)); + assertThat(CoreValuesSourceType.HISTOGRAM.ordinal(), equalTo(5)); } @Override @@ -47,6 +48,7 @@ public void testFromString() { assertThat(CoreValuesSourceType.fromString("bytes"), equalTo(CoreValuesSourceType.BYTES)); assertThat(CoreValuesSourceType.fromString("geopoint"), equalTo(CoreValuesSourceType.GEOPOINT)); assertThat(CoreValuesSourceType.fromString("range"), equalTo(CoreValuesSourceType.RANGE)); + assertThat(CoreValuesSourceType.fromString("histogram"), equalTo(CoreValuesSourceType.HISTOGRAM)); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> CoreValuesSourceType.fromString("does_not_exist")); assertThat(e.getMessage(), @@ -61,6 +63,7 @@ public void testReadFrom() throws IOException { assertReadFromStream(2, CoreValuesSourceType.BYTES); assertReadFromStream(3, CoreValuesSourceType.GEOPOINT); assertReadFromStream(4, CoreValuesSourceType.RANGE); + assertReadFromStream(5, CoreValuesSourceType.HISTOGRAM); } @Override @@ -70,5 +73,6 @@ public void testWriteTo() throws IOException { assertWriteToStream(CoreValuesSourceType.BYTES, 2); assertWriteToStream(CoreValuesSourceType.GEOPOINT, 3); assertWriteToStream(CoreValuesSourceType.RANGE, 4); + assertWriteToStream(CoreValuesSourceType.HISTOGRAM, 5); } } From 876c01f5102c7d7d7efb56d31c14eb780a1da8e5 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 2 Dec 2019 16:42:26 +0100 Subject: [PATCH 041/686] Make Snapshot Metadata Javadocs Clearer (#49697) We are always using the snapshot name on the shard level, lets make it crystal clear in the docs. --- .../snapshots/blobstore/BlobStoreIndexShardSnapshot.java | 9 ++++++--- .../index/snapshots/blobstore/SnapshotFiles.java | 9 +++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/BlobStoreIndexShardSnapshot.java b/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/BlobStoreIndexShardSnapshot.java index e2a2ce4428881..624510bb86f67 100644 --- a/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/BlobStoreIndexShardSnapshot.java +++ b/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/BlobStoreIndexShardSnapshot.java @@ -335,6 +335,9 @@ public String toString() { } } + /** + * Snapshot name + */ private final String snapshot; private final long indexVersion; @@ -352,7 +355,7 @@ public String toString() { /** * Constructs new shard snapshot metadata from snapshot metadata * - * @param snapshot snapshot id + * @param snapshot snapshot name * @param indexVersion index version * @param indexFiles list of files in the shard * @param startTime snapshot start time @@ -377,9 +380,9 @@ public BlobStoreIndexShardSnapshot(String snapshot, long indexVersion, List physicalFiles = null; + /** + * Returns snapshot name + * + * @return snapshot name + */ public String snapshot() { return snapshot; } + /** + * @param snapshot snapshot name + * @param indexFiles index files + */ public SnapshotFiles(String snapshot, List indexFiles ) { this.snapshot = snapshot; this.indexFiles = indexFiles; From d741c830fa70c12c018dc52bd153853adba7c8ae Mon Sep 17 00:00:00 2001 From: jimczi Date: Mon, 2 Dec 2019 20:15:46 +0100 Subject: [PATCH 042/686] add new version 7.5.1 --- server/src/main/java/org/elasticsearch/Version.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index 9fa0fc9180029..c39502f0b0e3d 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -69,8 +69,8 @@ public class Version implements Comparable, ToXContentFragment { public static final Version V_7_4_0 = new Version(7040099, org.apache.lucene.util.Version.LUCENE_8_2_0); public static final Version V_7_4_1 = new Version(7040199, org.apache.lucene.util.Version.LUCENE_8_2_0); public static final Version V_7_4_2 = new Version(7040299, org.apache.lucene.util.Version.LUCENE_8_2_0); - public static final Version V_7_4_3 = new Version(7040399, org.apache.lucene.util.Version.LUCENE_8_2_0); public static final Version V_7_5_0 = new Version(7050099, org.apache.lucene.util.Version.LUCENE_8_3_0); + public static final Version V_7_5_1 = new Version(7050199, org.apache.lucene.util.Version.LUCENE_8_3_0); public static final Version V_7_6_0 = new Version(7060099, org.apache.lucene.util.Version.LUCENE_8_4_0); public static final Version V_8_0_0 = new Version(8000099, org.apache.lucene.util.Version.LUCENE_8_4_0); public static final Version CURRENT = V_8_0_0; From b44ced920805d07b3ca674d6c7d6d68f78c3327d Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 2 Dec 2019 15:52:13 -0500 Subject: [PATCH 043/686] [DOCS] Document CCR compatibility requirements (#49776) * Creates a prerequisites section in the cross-cluster replication (CCR) overview. * Adds concise definitions for local and remote cluster in a CCR context. * Documents that the ES version of the local cluster must be the same or a newer compatible version as the remote cluster. --- docs/reference/ccr/overview.asciidoc | 13 ++++++++++++- docs/reference/modules/remote-clusters.asciidoc | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/reference/ccr/overview.asciidoc b/docs/reference/ccr/overview.asciidoc index 34902d7889299..7ea9fd210f6e9 100644 --- a/docs/reference/ccr/overview.asciidoc +++ b/docs/reference/ccr/overview.asciidoc @@ -17,7 +17,18 @@ Replication is pull-based. This means that replication is driven by the follower index. This simplifies state management on the leader index and means that {ccr} does not interfere with indexing on the leader index. -IMPORTANT: {ccr-cap} requires <>. +In {ccr}, the cluster performing this pull is known as the _local cluster_. The +cluster being replicated is known as the _remote cluster_. + +==== Prerequisites + +* {ccr-cap} requires <>. + +* The {es} version of the local cluster must be **the same as or newer** than +the remote cluster. If newer, the versions must also be compatible as outlined +in the following matrix. + +include::../modules/remote-clusters.asciidoc[tag=remote-cluster-compatibility-matrix] ==== Configuring replication diff --git a/docs/reference/modules/remote-clusters.asciidoc b/docs/reference/modules/remote-clusters.asciidoc index e7e820d71cf67..0d0b24f31e25d 100644 --- a/docs/reference/modules/remote-clusters.asciidoc +++ b/docs/reference/modules/remote-clusters.asciidoc @@ -43,6 +43,7 @@ node, while 6.7 can only communicate with 7.0. Version compatibility is symmetric, meaning that if 6.7 can communicate with 7.0, 7.0 can also communicate with 6.7. The matrix below summarizes compatibility as described above. +// tag::remote-cluster-compatibility-matrix[] [cols="^,^,^,^,^,^,^,^"] |==== | Compatibility | 5.0->5.5 | 5.6 | 6.0->6.6 | 6.7 | 6.8 | 7.0 | 7.1->7.x @@ -54,6 +55,7 @@ communicate with 6.7. The matrix below summarizes compatibility as described abo | 7.0 | No | No | No | Yes | Yes | Yes | Yes | 7.1->7.x | No | No | No | No | Yes | Yes | Yes |==== +// end::remote-cluster-compatibility-matrix[] - *role*: Dedicated master nodes never get selected. - *attributes*: You can tag which nodes should be selected From 585adfcc6d6f4ce4c267a04ad491deac57e3946f Mon Sep 17 00:00:00 2001 From: Mayya Sharipova Date: Mon, 2 Dec 2019 17:00:58 -0500 Subject: [PATCH 044/686] Mute testIndexHasDuplicateData (#49779) Related to #49703 --- .../java/org/elasticsearch/search/query/QueryPhaseTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/search/query/QueryPhaseTests.java b/server/src/test/java/org/elasticsearch/search/query/QueryPhaseTests.java index 2c9fc90c22cab..4c33177e268f9 100644 --- a/server/src/test/java/org/elasticsearch/search/query/QueryPhaseTests.java +++ b/server/src/test/java/org/elasticsearch/search/query/QueryPhaseTests.java @@ -703,6 +703,7 @@ public void testNumericLongOrDateSortOptimization() throws Exception { dir.close(); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/49703") public void testIndexHasDuplicateData() throws IOException { int docsCount = 7000; int duplIndex = docsCount * 7 / 10; From 617a9cea50679af866680045859647695ad6f008 Mon Sep 17 00:00:00 2001 From: Mayya Sharipova Date: Mon, 2 Dec 2019 17:01:30 -0500 Subject: [PATCH 045/686] Disable sort optimization when index is sorted (#49727) Don't run long sort optimization when index is already sorted on the same field as the sort query parameter. Relates to #37043, follow up for #48804 --- .../main/java/org/elasticsearch/search/query/QueryPhase.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/search/query/QueryPhase.java b/server/src/main/java/org/elasticsearch/search/query/QueryPhase.java index 7ec6bf3e68313..ba5ae725cab6f 100644 --- a/server/src/main/java/org/elasticsearch/search/query/QueryPhase.java +++ b/server/src/main/java/org/elasticsearch/search/query/QueryPhase.java @@ -412,6 +412,10 @@ private static Query tryRewriteLongSort(SearchContext searchContext, IndexReader if (searchContext.collapse() != null) return null; if (searchContext.trackScores()) return null; if (searchContext.aggregations() != null) return null; + if (canEarlyTerminate(reader, searchContext.sort())) { + // disable this optimization if index sorting matches the query sort since it's already optimized by index searcher + return null; + } Sort sort = searchContext.sort().sort; SortField sortField = sort.getSort()[0]; if (SortField.Type.LONG.equals(IndexSortConfig.getSortFieldType(sortField)) == false) return null; From 4a126abef2c43cd10c753f43488f0b921147fbb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Tue, 3 Dec 2019 08:19:33 +0100 Subject: [PATCH 046/686] Make only a part of `stop()` method a critical section. (#49756) --- .../dataframe/process/AnalyticsProcessManager.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java index 0a2c6440bf0cc..ed9d715b5f78c 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java @@ -93,7 +93,7 @@ public void runJob(DataFrameAnalyticsTask task, DataFrameAnalyticsConfig config, Consumer finishHandler) { executorServiceForJob.execute(() -> { ProcessContext processContext = new ProcessContext(config.getId()); - synchronized (this) { + synchronized (processContextByAllocation) { if (task.isStopping()) { // The task was requested to stop before we created the process context finishHandler.accept(null); @@ -295,14 +295,17 @@ private void closeProcess(DataFrameAnalyticsTask task) { processContext.process.close(); LOGGER.info("[{}] Closed process", configId); } catch (Exception e) { - String errorMsg = new ParameterizedMessage("[{}] Error closing data frame analyzer process [{}]" - , configId, e.getMessage()).getFormattedMessage(); + String errorMsg = new ParameterizedMessage( + "[{}] Error closing data frame analyzer process [{}]", configId, e.getMessage()).getFormattedMessage(); processContext.setFailureReason(errorMsg); } } - public synchronized void stop(DataFrameAnalyticsTask task) { - ProcessContext processContext = processContextByAllocation.get(task.getAllocationId()); + public void stop(DataFrameAnalyticsTask task) { + ProcessContext processContext; + synchronized (processContextByAllocation) { + processContext = processContextByAllocation.get(task.getAllocationId()); + } if (processContext != null) { LOGGER.debug("[{}] Stopping process", task.getParams().getId()); processContext.stop(); From 7d38dac63e144f25b606d1b7e5a3dcfaba78e00e Mon Sep 17 00:00:00 2001 From: Eric Miers Date: Tue, 3 Dec 2019 04:31:41 -0500 Subject: [PATCH 047/686] Update CONTRIBUTING.md to require Docker for some tasks (#49221) Updated the contributing guide to reflect that Docker is a necessary dependency for certain tasks. --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24d9b4a87c461..53935ec15feb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,6 +115,10 @@ We support development in the Eclipse and IntelliJ IDEs. For Eclipse, the minimum version that we support is [4.13][eclipse]. For IntelliJ, the minimum version that we support is [IntelliJ 2017.2][intellij]. +[Docker](https://docs.docker.com/install/) is required for building some Elasticsearch artifacts and executing certain test suites. You can run Elasticsearch without building all the artifacts with: + + ./gradlew :run + ### Configuring IDEs And Running Tests Eclipse users can automatically configure their IDE: `./gradlew eclipse` From 7733366ce217e4c5c93e7345dad2b476acd4994d Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Tue, 3 Dec 2019 09:43:54 +0000 Subject: [PATCH 048/686] Add healthchecks to distro docker-compose.yml (#49710) If there are environmental reasons why docker-compose can't bring up containers in the :distribution:docker project, it's not that clear from the command line output. Add healthchecks to the compose file so that the cluster's health is explicitly checked. Also add a note about Docker memory requirements to the testing docs. --- TESTING.asciidoc | 4 ++ distribution/docker/docker-compose.yml | 82 +++++++++++++++++--------- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/TESTING.asciidoc b/TESTING.asciidoc index 438dbe5ba5ce2..41cdc19e87f98 100644 --- a/TESTING.asciidoc +++ b/TESTING.asciidoc @@ -263,6 +263,10 @@ If you want to just run the precommit checks: Some of these checks will require `docker-compose` installed for bringing up test fixtures. If it's not present those checks will be skipped automatically. +The host running Docker (or VM if you're using Docker Desktop) needs 4GB of +memory or some of the containers will fail to start. You can tell that you +are short of memory if containers are exiting quickly after starting with +code 137 (128 + 9, where 9 means SIGKILL). == Testing the REST layer diff --git a/distribution/docker/docker-compose.yml b/distribution/docker/docker-compose.yml index acf24cbce28bd..245056382f304 100644 --- a/distribution/docker/docker-compose.yml +++ b/distribution/docker/docker-compose.yml @@ -1,12 +1,12 @@ # Only used for testing the docker images -version: '3' +version: '3.7' services: elasticsearch-default-1: image: elasticsearch:test - environment: - - node.name=elasticsearch-default-1 + environment: + - node.name=elasticsearch-default-1 - cluster.initial_master_nodes=elasticsearch-default-1,elasticsearch-default-2 - - discovery.seed_hosts=elasticsearch-default-2:9300 + - discovery.seed_hosts=elasticsearch-default-2:9300 - cluster.name=elasticsearch-default - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" @@ -16,20 +16,20 @@ services: - cluster.routing.allocation.disk.watermark.high=1b - cluster.routing.allocation.disk.watermark.flood_stage=1b - script.max_compilations_rate=2048/1m - - node.store.allow_mmap=false + - node.store.allow_mmap=false - xpack.security.enabled=true - xpack.security.transport.ssl.enabled=true - xpack.security.http.ssl.enabled=true - xpack.security.authc.token.enabled=true - xpack.security.audit.enabled=true - - xpack.security.authc.realms.file.file1.order=0 + - xpack.security.authc.realms.file.file1.order=0 - xpack.security.authc.realms.native.native1.order=1 - xpack.security.transport.ssl.keystore.path=/usr/share/elasticsearch/config/testnode.jks - xpack.security.http.ssl.keystore.path=/usr/share/elasticsearch/config/testnode.jks - - xpack.http.ssl.verification_mode=certificate - - xpack.security.transport.ssl.verification_mode=certificate + - xpack.http.ssl.verification_mode=certificate + - xpack.security.transport.ssl.verification_mode=certificate - xpack.license.self_generated.type=trial - volumes: + volumes: - ./build/repo:/tmp/es-repo - ./build/certs/testnode.jks:/usr/share/elasticsearch/config/testnode.jks - ./build/logs/default-1:/usr/share/elasticsearch/logs @@ -42,14 +42,20 @@ services: hard: -1 nofile: soft: 65536 - hard: 65536 + hard: 65536 entrypoint: /docker-test-entrypoint.sh + healthcheck: + start_period: 15s + test: ["CMD", "curl", "-f", "-u", "x_pack_rest_user:x-pack-test-password", "-k", "https://localhost:9200"] + interval: 10s + timeout: 2s + retries: 5 elasticsearch-default-2: image: elasticsearch:test - environment: + environment: - node.name=elasticsearch-default-2 - cluster.initial_master_nodes=elasticsearch-default-1,elasticsearch-default-2 - - discovery.seed_hosts=elasticsearch-default-1:9300 + - discovery.seed_hosts=elasticsearch-default-1:9300 - cluster.name=elasticsearch-default - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" @@ -59,20 +65,20 @@ services: - cluster.routing.allocation.disk.watermark.high=1b - cluster.routing.allocation.disk.watermark.flood_stage=1b - script.max_compilations_rate=2048/1m - - node.store.allow_mmap=false + - node.store.allow_mmap=false - xpack.security.enabled=true - xpack.security.transport.ssl.enabled=true - xpack.security.http.ssl.enabled=true - xpack.security.authc.token.enabled=true - xpack.security.audit.enabled=true - - xpack.security.authc.realms.file.file1.order=0 - - xpack.security.authc.realms.native.native1.order=1 + - xpack.security.authc.realms.file.file1.order=0 + - xpack.security.authc.realms.native.native1.order=1 - xpack.security.transport.ssl.keystore.path=/usr/share/elasticsearch/config/testnode.jks - xpack.security.http.ssl.keystore.path=/usr/share/elasticsearch/config/testnode.jks - - xpack.http.ssl.verification_mode=certificate - - xpack.security.transport.ssl.verification_mode=certificate + - xpack.http.ssl.verification_mode=certificate + - xpack.security.transport.ssl.verification_mode=certificate - xpack.license.self_generated.type=trial - volumes: + volumes: - ./build/repo:/tmp/es-repo - ./build/certs/testnode.jks:/usr/share/elasticsearch/config/testnode.jks - ./build/logs/default-2:/usr/share/elasticsearch/logs @@ -85,14 +91,20 @@ services: hard: -1 nofile: soft: 65536 - hard: 65536 + hard: 65536 entrypoint: /docker-test-entrypoint.sh + healthcheck: + start_period: 15s + test: ["CMD", "curl", "-f", "-u", "x_pack_rest_user:x-pack-test-password", "-k", "https://localhost:9200"] + interval: 10s + timeout: 2s + retries: 5 elasticsearch-oss-1: image: elasticsearch:test - environment: - - node.name=elasticsearch-oss-1 + environment: + - node.name=elasticsearch-oss-1 - cluster.initial_master_nodes=elasticsearch-oss-1,elasticsearch-oss-2 - - discovery.seed_hosts=elasticsearch-oss-2:9300 + - discovery.seed_hosts=elasticsearch-oss-2:9300 - cluster.name=elasticsearch-oss - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" @@ -102,8 +114,8 @@ services: - cluster.routing.allocation.disk.watermark.high=1b - cluster.routing.allocation.disk.watermark.flood_stage=1b - script.max_compilations_rate=2048/1m - - node.store.allow_mmap=false - volumes: + - node.store.allow_mmap=false + volumes: - ./build/oss-repo:/tmp/es-repo - ./build/logs/oss-1:/usr/share/elasticsearch/logs ports: @@ -114,13 +126,19 @@ services: hard: -1 nofile: soft: 65536 - hard: 65536 + hard: 65536 + healthcheck: + start_period: 15s + test: ["CMD", "curl", "-f", "http://localhost:9200"] + interval: 10s + timeout: 2s + retries: 5 elasticsearch-oss-2: image: elasticsearch:test - environment: + environment: - node.name=elasticsearch-oss-2 - cluster.initial_master_nodes=elasticsearch-oss-1,elasticsearch-oss-2 - - discovery.seed_hosts=elasticsearch-oss-1:9300 + - discovery.seed_hosts=elasticsearch-oss-1:9300 - cluster.name=elasticsearch-oss - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" @@ -130,8 +148,8 @@ services: - cluster.routing.allocation.disk.watermark.high=1b - cluster.routing.allocation.disk.watermark.flood_stage=1b - script.max_compilations_rate=2048/1m - - node.store.allow_mmap=false - volumes: + - node.store.allow_mmap=false + volumes: - ./build/oss-repo:/tmp/es-repo - ./build/logs/oss-2:/usr/share/elasticsearch/logs ports: @@ -140,3 +158,9 @@ services: memlock: soft: -1 hard: -1 + healthcheck: + start_period: 15s + test: ["CMD", "curl", "-f", "http://localhost:9200"] + interval: 10s + timeout: 2s + retries: 5 From 80d6e78e8a3f49738e1d535a5899d034ed36f2bc Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Tue, 3 Dec 2019 11:46:50 +0100 Subject: [PATCH 049/686] Replicate write actions before fsyncing them (#49746) This commit fixes a number of issues with data replication: - Local and global checkpoints are not updated after the new operations have been fsynced, but might capture a state before the fsync. The reason why this probably went undetected for so long is that AsyncIOProcessor is synchronous if you index one item at a time, and hence working as intended unless you have a high enough level of concurrent indexing. As we rely in other places on the assumption that we have an up-to-date local checkpoint in case of synchronous translog durability, there's a risk for the local and global checkpoints not to be up-to-date after replication completes, and that this won't be corrected by the periodic global checkpoint sync. - AsyncIOProcessor also has another "bad" side effect here: if you index one bulk at a time, the bulk is always first fsynced on the primary before being sent to the replica. Further, if one thread is tasked by AsyncIOProcessor to drain the processing queue and fsync, other threads can easily pile more bulk requests on top of that thread. Things are not very fair here, and the thread might continue doing a lot more fsyncs before returning (as the other threads pile more and more on top), which blocks it from returning as a replication request (e.g. if this thread is on the primary, it blocks the replication requests to the replicas from going out, and delaying checkpoint advancement). This commit fixes all these issues, and also simplifies the code that coordinates all the after write actions. --- .../replication/ReplicationOperation.java | 56 +++++++-- .../TransportReplicationAction.java | 101 +++++++-------- .../replication/TransportWriteAction.java | 118 +++++++----------- ...portVerifyShardBeforeCloseActionTests.java | 5 + .../ReplicationOperationTests.java | 11 ++ .../TransportWriteActionTests.java | 58 ++------- .../index/seqno/GlobalCheckpointSyncIT.java | 27 ++++ ...tentionLeaseBackgroundSyncActionTests.java | 2 +- .../seqno/RetentionLeaseSyncActionTests.java | 2 +- .../ESIndexLevelReplicationTestCase.java | 20 ++- .../TransportBulkShardOperationsAction.java | 32 ++--- .../ShardFollowTaskReplicationTests.java | 40 +++--- .../action/bulk/BulkShardOperationsTests.java | 7 +- 13 files changed, 241 insertions(+), 238 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/support/replication/ReplicationOperation.java b/server/src/main/java/org/elasticsearch/action/support/replication/ReplicationOperation.java index 8cea44911bbe7..e24a3dbe381c3 100644 --- a/server/src/main/java/org/elasticsearch/action/support/replication/ReplicationOperation.java +++ b/server/src/main/java/org/elasticsearch/action/support/replication/ReplicationOperation.java @@ -46,6 +46,7 @@ import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.LongSupplier; public class ReplicationOperation< Request extends ReplicationRequest, @@ -110,8 +111,6 @@ public void execute() throws Exception { private void handlePrimaryResult(final PrimaryResultT primaryResult) { this.primaryResult = primaryResult; - primary.updateLocalCheckpointForShard(primary.routingEntry().allocationId().getId(), primary.localCheckpoint()); - primary.updateGlobalCheckpointForShard(primary.routingEntry().allocationId().getId(), primary.globalCheckpoint()); final ReplicaRequest replicaRequest = primaryResult.replicaRequest(); if (replicaRequest != null) { if (logger.isTraceEnabled()) { @@ -134,8 +133,26 @@ private void handlePrimaryResult(final PrimaryResultT primaryResult) { markUnavailableShardsAsStale(replicaRequest, replicationGroup); performOnReplicas(replicaRequest, globalCheckpoint, maxSeqNoOfUpdatesOrDeletes, replicationGroup); } - successfulShards.incrementAndGet(); // mark primary as successful - decPendingAndFinishIfNeeded(); + primaryResult.runPostReplicationActions(new ActionListener<>() { + + @Override + public void onResponse(Void aVoid) { + successfulShards.incrementAndGet(); + try { + updateCheckPoints(primary.routingEntry(), primary::localCheckpoint, primary::globalCheckpoint); + } finally { + decPendingAndFinishIfNeeded(); + } + } + + @Override + public void onFailure(Exception e) { + logger.trace("[{}] op [{}] post replication actions failed for [{}]", primary.routingEntry().shardId(), opType, request); + // TODO: fail shard? This will otherwise have the local / global checkpoint info lagging, or possibly have replicas + // go out of sync with the primary + finishAsFailed(e); + } + }); } private void markUnavailableShardsAsStale(ReplicaRequest replicaRequest, ReplicationGroup replicationGroup) { @@ -176,16 +193,10 @@ private void performOnReplica(final ShardRouting shard, final ReplicaRequest rep public void onResponse(ReplicaResponse response) { successfulShards.incrementAndGet(); try { - primary.updateLocalCheckpointForShard(shard.allocationId().getId(), response.localCheckpoint()); - primary.updateGlobalCheckpointForShard(shard.allocationId().getId(), response.globalCheckpoint()); - } catch (final AlreadyClosedException e) { - // the index was deleted or this shard was never activated after a relocation; fall through and finish normally - } catch (final Exception e) { - // fail the primary but fall through and let the rest of operation processing complete - final String message = String.format(Locale.ROOT, "primary failed updating local checkpoint for replica %s", shard); - primary.failShard(message, e); + updateCheckPoints(shard, response::localCheckpoint, response::globalCheckpoint); + } finally { + decPendingAndFinishIfNeeded(); } - decPendingAndFinishIfNeeded(); } @Override @@ -211,6 +222,19 @@ public String toString() { }); } + private void updateCheckPoints(ShardRouting shard, LongSupplier localCheckpointSupplier, LongSupplier globalCheckpointSupplier) { + try { + primary.updateLocalCheckpointForShard(shard.allocationId().getId(), localCheckpointSupplier.getAsLong()); + primary.updateGlobalCheckpointForShard(shard.allocationId().getId(), globalCheckpointSupplier.getAsLong()); + } catch (final AlreadyClosedException e) { + // the index was deleted or this shard was never activated after a relocation; fall through and finish normally + } catch (final Exception e) { + // fail the primary but fall through and let the rest of operation processing complete + final String message = String.format(Locale.ROOT, "primary failed updating local checkpoint for replica %s", shard); + primary.failShard(message, e); + } + } + private void onNoLongerPrimary(Exception failure) { final Throwable cause = ExceptionsHelper.unwrapCause(failure); final boolean nodeIsClosing = cause instanceof NodeClosedException; @@ -464,5 +488,11 @@ public interface PrimaryResult> { @Nullable RequestT replicaRequest(); void setShardInfo(ReplicationResponse.ShardInfo shardInfo); + + /** + * Run actions to be triggered post replication + * @param listener calllback that is invoked after post replication actions have completed + * */ + void runPostReplicationActions(ActionListener listener); } } diff --git a/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java b/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java index c72f10b35c372..5888ec3f46bf6 100644 --- a/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java @@ -73,8 +73,6 @@ import org.elasticsearch.transport.TransportException; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestOptions; -import org.elasticsearch.transport.TransportResponse; -import org.elasticsearch.transport.TransportResponse.Empty; import org.elasticsearch.transport.TransportResponseHandler; import org.elasticsearch.transport.TransportService; @@ -346,17 +344,12 @@ public void handleException(TransportException exp) { } else { setPhase(replicationTask, "primary"); - final ActionListener referenceClosingListener = ActionListener.wrap(response -> { - primaryShardReference.close(); // release shard operation lock before responding to caller - setPhase(replicationTask, "finished"); - onCompletionListener.onResponse(response); - }, e -> handleException(primaryShardReference, e)); + final ActionListener responseListener = ActionListener.wrap(response -> { + adaptResponse(response, primaryShardReference.indexShard); - final ActionListener globalCheckpointSyncingListener = ActionListener.wrap(response -> { if (syncGlobalCheckpointAfterOperation) { - final IndexShard shard = primaryShardReference.indexShard; try { - shard.maybeSyncGlobalCheckpoint("post-operation"); + primaryShardReference.indexShard.maybeSyncGlobalCheckpoint("post-operation"); } catch (final Exception e) { // only log non-closed exceptions if (ExceptionsHelper.unwrap( @@ -364,15 +357,19 @@ public void handleException(TransportException exp) { // intentionally swallow, a missed global checkpoint sync should not fail this operation logger.info( new ParameterizedMessage( - "{} failed to execute post-operation global checkpoint sync", shard.shardId()), e); + "{} failed to execute post-operation global checkpoint sync", + primaryShardReference.indexShard.shardId()), e); } } } - referenceClosingListener.onResponse(response); - }, referenceClosingListener::onFailure); + + primaryShardReference.close(); // release shard operation lock before responding to caller + setPhase(replicationTask, "finished"); + onCompletionListener.onResponse(response); + }, e -> handleException(primaryShardReference, e)); new ReplicationOperation<>(primaryRequest.getRequest(), primaryShardReference, - ActionListener.wrap(result -> result.respond(globalCheckpointSyncingListener), referenceClosingListener::onFailure), + ActionListener.map(responseListener, result -> result.finalResponseIfSuccessful), newReplicasProxy(), logger, actionName, primaryRequest.getPrimaryTerm()).execute(); } } catch (Exception e) { @@ -393,10 +390,19 @@ public void onFailure(Exception e) { } + // allows subclasses to adapt the response + protected void adaptResponse(Response response, IndexShard indexShard) { + + } + + protected ActionListener wrapResponseActionListener(ActionListener listener, IndexShard shard) { + return listener; + } + public static class PrimaryResult, Response extends ReplicationResponse> implements ReplicationOperation.PrimaryResult { - final ReplicaRequest replicaRequest; + protected final ReplicaRequest replicaRequest; public final Response finalResponseIfSuccessful; public final Exception finalFailure; @@ -429,11 +435,12 @@ public void setShardInfo(ReplicationResponse.ShardInfo shardInfo) { } } - public void respond(ActionListener listener) { - if (finalResponseIfSuccessful != null) { - listener.onResponse(finalResponseIfSuccessful); - } else { + @Override + public void runPostReplicationActions(ActionListener listener) { + if (finalFailure != null) { listener.onFailure(finalFailure); + } else { + listener.onResponse(null); } } } @@ -449,11 +456,11 @@ public ReplicaResult() { this(null); } - public void respond(ActionListener listener) { - if (finalFailure == null) { - listener.onResponse(TransportResponse.Empty.INSTANCE); - } else { + public void runPostReplicaActions(ActionListener listener) { + if (finalFailure != null) { listener.onFailure(finalFailure); + } else { + listener.onResponse(null); } } } @@ -503,10 +510,23 @@ public void onResponse(Releasable releasable) { try { assert replica.getActiveOperationsCount() != 0 : "must perform shard operation under a permit"; final ReplicaResult replicaResult = shardOperationOnReplica(replicaRequest.getRequest(), replica); - releasable.close(); // release shard operation lock before responding to caller - final TransportReplicationAction.ReplicaResponse response = - new ReplicaResponse(replica.getLocalCheckpoint(), replica.getLastSyncedGlobalCheckpoint()); - replicaResult.respond(new ResponseListener(response)); + replicaResult.runPostReplicaActions( + ActionListener.wrap(r -> { + final TransportReplicationAction.ReplicaResponse response = + new ReplicaResponse(replica.getLocalCheckpoint(), replica.getLastSyncedGlobalCheckpoint()); + releasable.close(); // release shard operation lock before responding to caller + if (logger.isTraceEnabled()) { + logger.trace("action [{}] completed on shard [{}] for request [{}]", transportReplicaAction, + replicaRequest.getRequest().shardId(), + replicaRequest.getRequest()); + } + setPhase(task, "finished"); + onCompletionListener.onResponse(response); + }, e -> { + Releasables.closeWhileHandlingException(releasable); // release shard operation lock before responding to caller + this.responseWithFailure(e); + }) + ); } catch (final Exception e) { Releasables.closeWhileHandlingException(releasable); // release shard operation lock before responding to caller AsyncReplicaAction.this.onFailure(e); @@ -564,33 +584,6 @@ protected void doRun() throws Exception { acquireReplicaOperationPermit(replica, replicaRequest.getRequest(), this, replicaRequest.getPrimaryTerm(), replicaRequest.getGlobalCheckpoint(), replicaRequest.getMaxSeqNoOfUpdatesOrDeletes()); } - - /** - * Listens for the response on the replica and sends the response back to the primary. - */ - private class ResponseListener implements ActionListener { - private final ReplicaResponse replicaResponse; - - ResponseListener(ReplicaResponse replicaResponse) { - this.replicaResponse = replicaResponse; - } - - @Override - public void onResponse(Empty response) { - if (logger.isTraceEnabled()) { - logger.trace("action [{}] completed on shard [{}] for request [{}]", transportReplicaAction, - replicaRequest.getRequest().shardId(), - replicaRequest.getRequest()); - } - setPhase(task, "finished"); - onCompletionListener.onResponse(replicaResponse); - } - - @Override - public void onFailure(Exception e) { - responseWithFailure(e); - } - } } private IndexShard getIndexShard(final ShardId shardId) { diff --git a/server/src/main/java/org/elasticsearch/action/support/replication/TransportWriteAction.java b/server/src/main/java/org/elasticsearch/action/support/replication/TransportWriteAction.java index 104d3517815c0..07f8e96b4e796 100644 --- a/server/src/main/java/org/elasticsearch/action/support/replication/TransportWriteAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/replication/TransportWriteAction.java @@ -41,7 +41,6 @@ import org.elasticsearch.index.translog.Translog.Location; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.TransportResponse; import org.elasticsearch.transport.TransportService; import java.util.concurrent.atomic.AtomicBoolean; @@ -124,12 +123,10 @@ protected abstract WriteReplicaResult shardOperationOnReplica( * NOTE: public for testing */ public static class WritePrimaryResult, - Response extends ReplicationResponse & WriteResponse> extends PrimaryResult - implements RespondingWriteResult { - boolean finishedAsyncActions; + Response extends ReplicationResponse & WriteResponse> extends PrimaryResult { public final Location location; public final IndexShard primary; - ActionListener listener = null; + private final Logger logger; public WritePrimaryResult(ReplicaRequest request, @Nullable Response finalResponse, @Nullable Location location, @Nullable Exception operationFailure, @@ -137,104 +134,73 @@ public WritePrimaryResult(ReplicaRequest request, @Nullable Response finalRespon super(request, finalResponse, operationFailure); this.location = location; this.primary = primary; + this.logger = logger; assert location == null || operationFailure == null : "expected either failure to be null or translog location to be null, " + "but found: [" + location + "] translog location and [" + operationFailure + "] failure"; - if (operationFailure != null) { - this.finishedAsyncActions = true; + } + + @Override + public void runPostReplicationActions(ActionListener listener) { + if (finalFailure != null) { + listener.onFailure(finalFailure); } else { /* - * We call this before replication because this might wait for a refresh and that can take a while. + * We call this after replication because this might wait for a refresh and that can take a while. * This way we wait for the refresh in parallel on the primary and on the replica. */ - new AsyncAfterWriteAction(primary, request, location, this, logger).run(); - } - } - - @Override - public synchronized void respond(ActionListener listener) { - this.listener = listener; - respondIfPossible(null); - } + new AsyncAfterWriteAction(primary, replicaRequest, location, new RespondingWriteResult() { + @Override + public void onSuccess(boolean forcedRefresh) { + finalResponseIfSuccessful.setForcedRefresh(forcedRefresh); + listener.onResponse(null); + } - /** - * Respond if the refresh has occurred and the listener is ready. Always called while synchronized on {@code this}. - */ - protected void respondIfPossible(Exception ex) { - assert Thread.holdsLock(this); - if (finishedAsyncActions && listener != null) { - if (ex == null) { - super.respond(listener); - } else { - listener.onFailure(ex); - } + @Override + public void onFailure(Exception ex) { + listener.onFailure(ex); + } + }, logger).run(); } } - - public synchronized void onFailure(Exception exception) { - finishedAsyncActions = true; - respondIfPossible(exception); - } - - @Override - public synchronized void onSuccess(boolean forcedRefresh) { - finalResponseIfSuccessful.setForcedRefresh(forcedRefresh); - finishedAsyncActions = true; - respondIfPossible(null); - } } /** * Result of taking the action on the replica. */ - public static class WriteReplicaResult> - extends ReplicaResult implements RespondingWriteResult { + public static class WriteReplicaResult> extends ReplicaResult { public final Location location; - boolean finishedAsyncActions; - private ActionListener listener; + private final ReplicaRequest request; + private final IndexShard replica; + private final Logger logger; public WriteReplicaResult(ReplicaRequest request, @Nullable Location location, @Nullable Exception operationFailure, IndexShard replica, Logger logger) { super(operationFailure); this.location = location; - if (operationFailure != null) { - this.finishedAsyncActions = true; - } else { - new AsyncAfterWriteAction(replica, request, location, this, logger).run(); - } + this.request = request; + this.replica = replica; + this.logger = logger; } @Override - public synchronized void respond(ActionListener listener) { - this.listener = listener; - respondIfPossible(null); - } + public void runPostReplicaActions(ActionListener listener) { + if (finalFailure != null) { + listener.onFailure(finalFailure); + } else { + new AsyncAfterWriteAction(replica, request, location, new RespondingWriteResult() { + @Override + public void onSuccess(boolean forcedRefresh) { + listener.onResponse(null); + } - /** - * Respond if the refresh has occurred and the listener is ready. Always called while synchronized on {@code this}. - */ - protected void respondIfPossible(Exception ex) { - assert Thread.holdsLock(this); - if (finishedAsyncActions && listener != null) { - if (ex == null) { - super.respond(listener); - } else { - listener.onFailure(ex); - } + @Override + public void onFailure(Exception ex) { + listener.onFailure(ex); + } + }, logger).run(); } } - - @Override - public synchronized void onFailure(Exception ex) { - finishedAsyncActions = true; - respondIfPossible(ex); - } - - @Override - public synchronized void onSuccess(boolean forcedRefresh) { - finishedAsyncActions = true; - respondIfPossible(null); - } } @Override diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseActionTests.java index ba48eabadcca4..3700722a5d634 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/close/TransportVerifyShardBeforeCloseActionTests.java @@ -348,6 +348,11 @@ public void setShardInfo(ReplicationResponse.ShardInfo shardInfo) { this.shardInfo.set(shardInfo); } + @Override + public void runPostReplicationActions(ActionListener listener) { + listener.onResponse(null); + } + public ReplicationResponse.ShardInfo getShardInfo() { return shardInfo.get(); } diff --git a/server/src/test/java/org/elasticsearch/action/support/replication/ReplicationOperationTests.java b/server/src/test/java/org/elasticsearch/action/support/replication/ReplicationOperationTests.java index 3038153a3d537..32051f51fce2f 100644 --- a/server/src/test/java/org/elasticsearch/action/support/replication/ReplicationOperationTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/replication/ReplicationOperationTests.java @@ -124,6 +124,7 @@ public void testReplication() throws Exception { assertThat(request.processedOnReplicas, equalTo(expectedReplicas)); assertThat(replicasProxy.failedReplicas, equalTo(simulatedFailures.keySet())); assertThat(replicasProxy.markedAsStaleCopies, equalTo(staleAllocationIds)); + assertThat("post replication operations not run on primary", request.runPostReplicationActionsOnPrimary.get(), equalTo(true)); assertTrue("listener is not marked as done", listener.isDone()); ShardInfo shardInfo = listener.actionGet().getShardInfo(); assertThat(shardInfo.getFailed(), equalTo(reportedFailures.size())); @@ -437,6 +438,7 @@ private Set getExpectedReplicas(ShardId shardId, ClusterState stat public static class Request extends ReplicationRequest { public AtomicBoolean processedOnPrimary = new AtomicBoolean(); + public AtomicBoolean runPostReplicationActionsOnPrimary = new AtomicBoolean(); public Set processedOnReplicas = ConcurrentCollections.newConcurrentSet(); Request(ShardId shardId) { @@ -505,6 +507,14 @@ public void setShardInfo(ShardInfo shardInfo) { this.shardInfo = shardInfo; } + @Override + public void runPostReplicationActions(ActionListener listener) { + if (request.runPostReplicationActionsOnPrimary.compareAndSet(false, true) == false) { + fail("processed [" + request + "] twice"); + } + listener.onResponse(null); + } + public ShardInfo getShardInfo() { return shardInfo; } @@ -597,6 +607,7 @@ public void performOn( final long maxSeqNoOfUpdatesOrDeletes, final ActionListener listener) { assertTrue("replica request processed twice on [" + replica + "]", request.processedOnReplicas.add(replica)); + assertFalse("primary post replication actions should run after replication", request.runPostReplicationActionsOnPrimary.get()); if (opFailures.containsKey(replica)) { listener.onFailure(opFailures.get(replica)); } else { diff --git a/server/src/test/java/org/elasticsearch/action/support/replication/TransportWriteActionTests.java b/server/src/test/java/org/elasticsearch/action/support/replication/TransportWriteActionTests.java index c22613b9c291f..441d532652a36 100644 --- a/server/src/test/java/org/elasticsearch/action/support/replication/TransportWriteActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/replication/TransportWriteActionTests.java @@ -67,9 +67,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.Locale; -import java.util.concurrent.BrokenBarrierException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -143,7 +140,7 @@ public void testPrimaryNoRefreshCall() throws Exception { testAction.shardOperationOnPrimary(request, indexShard, ActionTestUtils.assertNoFailureListener(result -> { CapturingActionListener listener = new CapturingActionListener<>(); - result.respond(listener); + result.runPostReplicationActions(ActionListener.map(listener, ignore -> result.finalResponseIfSuccessful)); assertNotNull(listener.response); assertNull(listener.failure); verify(indexShard, never()).refresh(any()); @@ -158,7 +155,7 @@ public void testReplicaNoRefreshCall() throws Exception { TransportWriteAction.WriteReplicaResult result = testAction.shardOperationOnReplica(request, indexShard); CapturingActionListener listener = new CapturingActionListener<>(); - result.respond(listener); + result.runPostReplicaActions(ActionListener.map(listener, ignore -> TransportResponse.Empty.INSTANCE)); assertNotNull(listener.response); assertNull(listener.failure); verify(indexShard, never()).refresh(any()); @@ -172,7 +169,7 @@ public void testPrimaryImmediateRefresh() throws Exception { testAction.shardOperationOnPrimary(request, indexShard, ActionTestUtils.assertNoFailureListener(result -> { CapturingActionListener listener = new CapturingActionListener<>(); - result.respond(listener); + result.runPostReplicationActions(ActionListener.map(listener, ignore -> result.finalResponseIfSuccessful)); assertNotNull(listener.response); assertNull(listener.failure); assertTrue(listener.response.forcedRefresh); @@ -188,7 +185,7 @@ public void testReplicaImmediateRefresh() throws Exception { TransportWriteAction.WriteReplicaResult result = testAction.shardOperationOnReplica(request, indexShard); CapturingActionListener listener = new CapturingActionListener<>(); - result.respond(listener); + result.runPostReplicaActions(ActionListener.map(listener, ignore -> TransportResponse.Empty.INSTANCE)); assertNotNull(listener.response); assertNull(listener.failure); verify(indexShard).refresh("refresh_flag_index"); @@ -203,7 +200,7 @@ public void testPrimaryWaitForRefresh() throws Exception { testAction.shardOperationOnPrimary(request, indexShard, ActionTestUtils.assertNoFailureListener(result -> { CapturingActionListener listener = new CapturingActionListener<>(); - result.respond(listener); + result.runPostReplicationActions(ActionListener.map(listener, ignore -> result.finalResponseIfSuccessful)); assertNull(listener.response); // Haven't really responded yet @SuppressWarnings({"unchecked", "rawtypes"}) @@ -226,7 +223,7 @@ public void testReplicaWaitForRefresh() throws Exception { TestAction testAction = new TestAction(); TransportWriteAction.WriteReplicaResult result = testAction.shardOperationOnReplica(request, indexShard); CapturingActionListener listener = new CapturingActionListener<>(); - result.respond(listener); + result.runPostReplicaActions(ActionListener.map(listener, ignore -> TransportResponse.Empty.INSTANCE)); assertNull(listener.response); // Haven't responded yet @SuppressWarnings({ "unchecked", "rawtypes" }) ArgumentCaptor> refreshListener = ArgumentCaptor.forClass((Class) Consumer.class); @@ -244,9 +241,9 @@ public void testDocumentFailureInShardOperationOnPrimary() throws Exception { TestRequest request = new TestRequest(); TestAction testAction = new TestAction(true, true); testAction.shardOperationOnPrimary(request, indexShard, - ActionTestUtils.assertNoFailureListener(writePrimaryResult -> { + ActionTestUtils.assertNoFailureListener(result -> { CapturingActionListener listener = new CapturingActionListener<>(); - writePrimaryResult.respond(listener); + result.runPostReplicationActions(ActionListener.map(listener, ignore -> result.finalResponseIfSuccessful)); assertNull(listener.response); assertNotNull(listener.failure); })); @@ -255,10 +252,10 @@ public void testDocumentFailureInShardOperationOnPrimary() throws Exception { public void testDocumentFailureInShardOperationOnReplica() throws Exception { TestRequest request = new TestRequest(); TestAction testAction = new TestAction(randomBoolean(), true); - TransportWriteAction.WriteReplicaResult writeReplicaResult = + TransportWriteAction.WriteReplicaResult result = testAction.shardOperationOnReplica(request, indexShard); CapturingActionListener listener = new CapturingActionListener<>(); - writeReplicaResult.respond(listener); + result.runPostReplicaActions(ActionListener.map(listener, ignore -> TransportResponse.Empty.INSTANCE)); assertNull(listener.response); assertNotNull(listener.failure); } @@ -350,41 +347,6 @@ public void testReplicaProxy() throws InterruptedException, ExecutionException { } } - public void testConcurrentWriteReplicaResultCompletion() throws InterruptedException { - IndexShard replica = mock(IndexShard.class); - when(replica.getTranslogDurability()).thenReturn(Translog.Durability.ASYNC); - TestRequest request = new TestRequest(); - request.setRefreshPolicy(RefreshPolicy.WAIT_UNTIL); - TransportWriteAction.WriteReplicaResult replicaResult = new TransportWriteAction.WriteReplicaResult<>( - request, new Translog.Location(0, 0, 0), null, replica, logger); - CyclicBarrier barrier = new CyclicBarrier(2); - Runnable waitForBarrier = () -> { - try { - barrier.await(); - } catch (InterruptedException | BrokenBarrierException e) { - throw new AssertionError(e); - } - }; - CountDownLatch completionLatch = new CountDownLatch(1); - threadPool.generic().execute(() -> { - waitForBarrier.run(); - replicaResult.respond(ActionListener.wrap(completionLatch::countDown)); - }); - if (randomBoolean()) { - threadPool.generic().execute(() -> { - waitForBarrier.run(); - replicaResult.onFailure(null); - }); - } else { - threadPool.generic().execute(() -> { - waitForBarrier.run(); - replicaResult.onSuccess(false); - }); - } - - assertTrue(completionLatch.await(30, TimeUnit.SECONDS)); - } - private class TestAction extends TransportWriteAction { private final boolean withDocumentFailureOnPrimary; diff --git a/server/src/test/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncIT.java b/server/src/test/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncIT.java index a714870953ae7..ad2520248a670 100644 --- a/server/src/test/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncIT.java +++ b/server/src/test/java/org/elasticsearch/index/seqno/GlobalCheckpointSyncIT.java @@ -259,4 +259,31 @@ public void testPersistGlobalCheckpoint() throws Exception { } }); } + + public void testPersistLocalCheckpoint() { + internalCluster().ensureAtLeastNumDataNodes(2); + Settings.Builder indexSettings = Settings.builder() + .put(IndexService.GLOBAL_CHECKPOINT_SYNC_INTERVAL_SETTING.getKey(), "10m") + .put(IndexSettings.INDEX_TRANSLOG_DURABILITY_SETTING.getKey(), Translog.Durability.REQUEST) + .put("index.number_of_shards", 1) + .put("index.number_of_replicas", randomIntBetween(0, 1)); + prepareCreate("test", indexSettings).get(); + ensureGreen("test"); + int numDocs = randomIntBetween(1, 20); + logger.info("numDocs {}", numDocs); + long maxSeqNo = 0; + for (int i = 0; i < numDocs; i++) { + maxSeqNo = client().prepareIndex("test").setId(Integer.toString(i)).setSource("{}", XContentType.JSON).get().getSeqNo(); + logger.info("got {}", maxSeqNo); + } + for (IndicesService indicesService : internalCluster().getDataNodeInstances(IndicesService.class)) { + for (IndexService indexService : indicesService) { + for (IndexShard shard : indexService) { + final SeqNoStats seqNoStats = shard.seqNoStats(); + assertThat(maxSeqNo, equalTo(seqNoStats.getMaxSeqNo())); + assertThat(seqNoStats.getLocalCheckpoint(), equalTo(seqNoStats.getMaxSeqNo()));; + } + } + } + } } diff --git a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncActionTests.java b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncActionTests.java index 282170a58e3e3..faccbc7ff482f 100644 --- a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncActionTests.java +++ b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseBackgroundSyncActionTests.java @@ -154,7 +154,7 @@ public void testRetentionLeaseBackgroundSyncActionOnReplica() throws WriteStateE verify(indexShard).persistRetentionLeases(); // the result should indicate success final AtomicBoolean success = new AtomicBoolean(); - result.respond(ActionListener.wrap(r -> success.set(true), e -> fail(e.toString()))); + result.runPostReplicaActions(ActionListener.wrap(r -> success.set(true), e -> fail(e.toString()))); assertTrue(success.get()); } diff --git a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseSyncActionTests.java b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseSyncActionTests.java index a63722d93666a..1999873464ddd 100644 --- a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseSyncActionTests.java +++ b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseSyncActionTests.java @@ -149,7 +149,7 @@ public void testRetentionLeaseSyncActionOnReplica() throws WriteStateException { verify(indexShard).persistRetentionLeases(); // the result should indicate success final AtomicBoolean success = new AtomicBoolean(); - result.respond(ActionListener.wrap(r -> success.set(true), e -> fail(e.toString()))); + result.runPostReplicaActions(ActionListener.wrap(r -> success.set(true), e -> fail(e.toString()))); assertTrue(success.get()); } diff --git a/test/framework/src/main/java/org/elasticsearch/index/replication/ESIndexLevelReplicationTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/replication/ESIndexLevelReplicationTestCase.java index 62f624335c864..c067c0717dcce 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/replication/ESIndexLevelReplicationTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/replication/ESIndexLevelReplicationTestCase.java @@ -601,14 +601,23 @@ protected ReplicationAction(Request request, ActionListener listener, public void execute() { try { new ReplicationOperation<>(request, new PrimaryRef(), - ActionListener.wrap(result -> result.respond(listener), listener::onFailure), new ReplicasRef(), logger, opType, - primaryTerm).execute(); + ActionListener.map(listener, result -> { + adaptResponse(result.finalResponse, getPrimaryShard()); + return result.finalResponse; + }), + new ReplicasRef(), logger, opType, primaryTerm) + .execute(); } catch (Exception e) { listener.onFailure(e); } } - IndexShard getPrimaryShard() { + // to be overridden by subclasses + protected void adaptResponse(Response response, IndexShard indexShard) { + + } + + protected IndexShard getPrimaryShard() { return replicationTargets.primary; } @@ -731,8 +740,9 @@ public void setShardInfo(ReplicationResponse.ShardInfo shardInfo) { finalResponse.setShardInfo(shardInfo); } - public void respond(ActionListener listener) { - listener.onResponse(finalResponse); + @Override + public void runPostReplicationActions(ActionListener listener) { + listener.onResponse(null); } } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/bulk/TransportBulkShardOperationsAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/bulk/TransportBulkShardOperationsAction.java index 8c4374fac3629..48f41ef527de1 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/bulk/TransportBulkShardOperationsAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/bulk/TransportBulkShardOperationsAction.java @@ -102,7 +102,7 @@ public static Translog.Operation rewriteOperationWithPrimaryTerm(Translog.Operat } // public for testing purposes only - public static CcrWritePrimaryResult shardOperationOnPrimary( + public static WritePrimaryResult shardOperationOnPrimary( final ShardId shardId, final String historyUUID, final List sourceOperations, @@ -154,7 +154,7 @@ public static CcrWritePrimaryResult shardOperationOnPrimary( } final BulkShardOperationsRequest replicaRequest = new BulkShardOperationsRequest( shardId, historyUUID, appliedOperations, maxSeqNoOfUpdatesOrDeletes); - return new CcrWritePrimaryResult(replicaRequest, location, primary, logger); + return new WritePrimaryResult<>(replicaRequest, new BulkShardOperationsResponse(), location, null, primary, logger); } @Override @@ -190,26 +190,16 @@ protected BulkShardOperationsResponse newResponseInstance(StreamInput in) throws return new BulkShardOperationsResponse(in); } - /** - * Custom write result to include global checkpoint after ops have been replicated. - */ - static final class CcrWritePrimaryResult extends WritePrimaryResult { - CcrWritePrimaryResult(BulkShardOperationsRequest request, Translog.Location location, IndexShard primary, Logger logger) { - super(request, new BulkShardOperationsResponse(), location, null, primary, logger); - } - - @Override - public synchronized void respond(ActionListener listener) { - final ActionListener wrappedListener = ActionListener.wrap(response -> { - final SeqNoStats seqNoStats = primary.seqNoStats(); - // return a fresh global checkpoint after the operations have been replicated for the shard follow task - response.setGlobalCheckpoint(seqNoStats.getGlobalCheckpoint()); - response.setMaxSeqNo(seqNoStats.getMaxSeqNo()); - listener.onResponse(response); - }, listener::onFailure); - super.respond(wrappedListener); - } + @Override + protected void adaptResponse(BulkShardOperationsResponse response, IndexShard indexShard) { + adaptBulkShardOperationsResponse(response, indexShard); + } + public static void adaptBulkShardOperationsResponse(BulkShardOperationsResponse response, IndexShard indexShard) { + final SeqNoStats seqNoStats = indexShard.seqNoStats(); + // return a fresh global checkpoint after the operations have been replicated for the shard follow task + response.setGlobalCheckpoint(seqNoStats.getGlobalCheckpoint()); + response.setMaxSeqNo(seqNoStats.getMaxSeqNo()); } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskReplicationTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskReplicationTests.java index 3fdf16a67946f..aebf4129c1afb 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskReplicationTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskReplicationTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.replication.ReplicationResponse; import org.elasticsearch.action.support.replication.TransportWriteAction; +import org.elasticsearch.action.support.replication.TransportWriteActionTestHelper; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.routing.RecoverySource; @@ -67,6 +68,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; @@ -668,26 +670,32 @@ class CcrAction extends ReplicationAction listener) { - ActionListener.completeWith(listener, () -> { - final PlainActionFuture permitFuture = new PlainActionFuture<>(); - primary.acquirePrimaryOperationPermit(permitFuture, ThreadPool.Names.SAME, request); - final TransportWriteAction.WritePrimaryResult ccrResult; - try (Releasable ignored = permitFuture.get()) { - ccrResult = TransportBulkShardOperationsAction.shardOperationOnPrimary(primary.shardId(), request.getHistoryUUID(), - request.getOperations(), request.getMaxSeqNoOfUpdatesOrDeletes(), primary, logger); - } - return new PrimaryResult(ccrResult.replicaRequest(), ccrResult.finalResponseIfSuccessful) { - @Override - public void respond(ActionListener listener) { - ccrResult.respond(listener); - } - }; - }); + final PlainActionFuture permitFuture = new PlainActionFuture<>(); + primary.acquirePrimaryOperationPermit(permitFuture, ThreadPool.Names.SAME, request); + final TransportWriteAction.WritePrimaryResult ccrResult; + try (Releasable ignored = permitFuture.get()) { + ccrResult = TransportBulkShardOperationsAction.shardOperationOnPrimary(primary.shardId(), request.getHistoryUUID(), + request.getOperations(), request.getMaxSeqNoOfUpdatesOrDeletes(), primary, logger); + TransportWriteActionTestHelper.performPostWriteActions(primary, request, ccrResult.location, logger); + } catch (InterruptedException | ExecutionException | IOException e) { + throw new AssertionError(e); + } + listener.onResponse(new PrimaryResult(ccrResult.replicaRequest(), ccrResult.finalResponseIfSuccessful)); + } + + @Override + protected void adaptResponse(BulkShardOperationsResponse response, IndexShard indexShard) { + TransportBulkShardOperationsAction.adaptBulkShardOperationsResponse(response, indexShard); } @Override protected void performOnReplica(BulkShardOperationsRequest request, IndexShard replica) throws Exception { - TransportBulkShardOperationsAction.shardOperationOnReplica(request, replica, logger); + try (Releasable ignored = PlainActionFuture.get(f -> replica.acquireReplicaOperationPermit( + getPrimaryShard().getPendingPrimaryTerm(), getPrimaryShard().getLastKnownGlobalCheckpoint(), + getPrimaryShard().getMaxSeqNoOfUpdatesOrDeletes(), f, ThreadPool.Names.SAME, request))) { + Translog.Location location = TransportBulkShardOperationsAction.shardOperationOnReplica(request, replica, logger).location; + TransportWriteActionTestHelper.performPostWriteActions(replica, request, location, logger); + } } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java index 850efa9a74b68..b14403406dd66 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java @@ -33,7 +33,6 @@ import static java.util.Collections.emptySet; import static org.elasticsearch.xpack.ccr.action.bulk.TransportBulkShardOperationsAction.rewriteOperationWithPrimaryTerm; import static org.hamcrest.Matchers.equalTo; -import static org.elasticsearch.xpack.ccr.action.bulk.TransportBulkShardOperationsAction.CcrWritePrimaryResult; public class BulkShardOperationsTests extends IndexShardTestCase { @@ -124,7 +123,8 @@ public void testPrimaryResultIncludeOnlyAppliedOperations() throws Exception { Randomness.shuffle(firstBulk); Randomness.shuffle(secondBulk); oldPrimary.advanceMaxSeqNoOfUpdatesOrDeletes(seqno); - final CcrWritePrimaryResult fullResult = TransportBulkShardOperationsAction.shardOperationOnPrimary(oldPrimary.shardId(), + final TransportWriteAction.WritePrimaryResult fullResult = + TransportBulkShardOperationsAction.shardOperationOnPrimary(oldPrimary.shardId(), oldPrimary.getHistoryUUID(), firstBulk, seqno, oldPrimary, logger); assertThat(fullResult.replicaRequest().getOperations(), equalTo(firstBulk.stream().map(op -> rewriteOperationWithPrimaryTerm(op, oldPrimaryTerm)).collect(Collectors.toList()))); @@ -138,7 +138,8 @@ public void testPrimaryResultIncludeOnlyAppliedOperations() throws Exception { // The second bulk includes some operations from the first bulk which were processed already; // only a subset of these operations will be included the result but with the old primary term. final List existingOps = randomSubsetOf(firstBulk); - final CcrWritePrimaryResult partialResult = TransportBulkShardOperationsAction.shardOperationOnPrimary(newPrimary.shardId(), + final TransportWriteAction.WritePrimaryResult partialResult = + TransportBulkShardOperationsAction.shardOperationOnPrimary(newPrimary.shardId(), newPrimary.getHistoryUUID(), Stream.concat(secondBulk.stream(), existingOps.stream()).collect(Collectors.toList()), seqno, newPrimary, logger); final long newPrimaryTerm = newPrimary.getOperationPrimaryTerm(); From 7b22f12288f66968e37adcc191e800524a9f0d0c Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Tue, 3 Dec 2019 11:47:27 +0100 Subject: [PATCH 050/686] Docs: Fix & test more grok processor documentation (#49447) The documentation contained a small error, as bytes and duration was not properly converted to a number and thus remained a string. The documentation is now also properly tested by providing a full blown simulate pipeline example. --- .../reference/ingest/processors/grok.asciidoc | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/docs/reference/ingest/processors/grok.asciidoc b/docs/reference/ingest/processors/grok.asciidoc index c1f2ae9645a38..c710bffde95b3 100644 --- a/docs/reference/ingest/processors/grok.asciidoc +++ b/docs/reference/ingest/processors/grok.asciidoc @@ -68,53 +68,58 @@ include::common-options.asciidoc[] Here is an example of using the provided patterns to extract out and name structured fields from a string field in a document. -[source,js] --------------------------------------------------- -{ - "message": "55.3.244.1 GET /index.html 15824 0.043" -} --------------------------------------------------- -// NOTCONSOLE - -The pattern for this could be: - -[source,txt] --------------------------------------------------- -%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:bytes} %{NUMBER:duration} --------------------------------------------------- - -Here is an example pipeline for processing the above document by using Grok: - -[source,js] +[source,console] -------------------------------------------------- +POST _ingest/pipeline/_simulate { - "description" : "...", - "processors": [ + "pipeline": { + "description" : "...", + "processors": [ + { + "grok": { + "field": "message", + "patterns": ["%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:bytes:int} %{NUMBER:duration:double}"] + } + } + ] + }, + "docs":[ { - "grok": { - "field": "message", - "patterns": ["%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:bytes} %{NUMBER:duration}"] + "_source": { + "message": "55.3.244.1 GET /index.html 15824 0.043" } } ] } -------------------------------------------------- -// NOTCONSOLE This pipeline will insert these named captures as new fields within the document, like so: -[source,js] +[source,console-result] -------------------------------------------------- { - "message": "55.3.244.1 GET /index.html 15824 0.043", - "client": "55.3.244.1", - "method": "GET", - "request": "/index.html", - "bytes": 15824, - "duration": "0.043" + "docs": [ + { + "doc": { + "_index": "_index", + "_id": "_id", + "_source" : { + "duration" : 0.043, + "request" : "/index.html", + "method" : "GET", + "bytes" : 15824, + "client" : "55.3.244.1", + "message" : "55.3.244.1 GET /index.html 15824 0.043" + }, + "_ingest": { + "timestamp": "2016-11-08T19:43:03.850+0000" + } + } + } + ] } -------------------------------------------------- -// NOTCONSOLE +// TESTRESPONSE[s/2016-11-08T19:43:03.850\+0000/$body.docs.0.doc._ingest.timestamp/] [[custom-patterns]] ==== Custom Patterns From 1d75b5a7c70b3549dc5d9e2612f4a3556504c06a Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Tue, 3 Dec 2019 10:56:48 +0000 Subject: [PATCH 051/686] Migrate some of the Docker tests from old repository (#49079) Reimplement a number of the tests from elastic/elasticsearch-docker. There is also one Docker image fix here, which is that two of the provided config files had different file permissions to the rest. I've fixed this with another RUN chmod while building the image, and adjusted the corresponding packaging test. --- distribution/docker/src/docker/Dockerfile | 1 + .../src/docker/bin/docker-entrypoint.sh | 3 +- qa/os/build.gradle | 4 + .../packaging/test/DockerTests.java | 301 +++++++++++++++--- .../elasticsearch/packaging/util/Docker.java | 128 +++++--- 5 files changed, 334 insertions(+), 103 deletions(-) diff --git a/distribution/docker/src/docker/Dockerfile b/distribution/docker/src/docker/Dockerfile index 0b36b29d8e22a..3a42a3e717185 100644 --- a/distribution/docker/src/docker/Dockerfile +++ b/distribution/docker/src/docker/Dockerfile @@ -33,6 +33,7 @@ RUN grep ES_DISTRIBUTION_TYPE=tar /usr/share/elasticsearch/bin/elasticsearch-env RUN mkdir -p config data logs RUN chmod 0775 config data logs COPY config/elasticsearch.yml config/log4j2.properties config/ +RUN chmod 0660 config/elasticsearch.yml config/log4j2.properties ################################################################################ # Build stage 1 (the actual elasticsearch image): diff --git a/distribution/docker/src/docker/bin/docker-entrypoint.sh b/distribution/docker/src/docker/bin/docker-entrypoint.sh index d95c451dead9d..cd418a5415ddc 100644 --- a/distribution/docker/src/docker/bin/docker-entrypoint.sh +++ b/distribution/docker/src/docker/bin/docker-entrypoint.sh @@ -85,8 +85,7 @@ declare -a es_opts while IFS='=' read -r envvar_key envvar_value do # Elasticsearch settings need to have at least two dot separated lowercase - # words, e.g. `cluster.name`, except for `processors` which we handle - # specially + # words, e.g. `cluster.name` if [[ "$envvar_key" =~ ^[a-z0-9_]+\.[a-z0-9_]+ ]]; then if [[ ! -z $envvar_value ]]; then es_opt="-E${envvar_key}=${envvar_value}" diff --git a/qa/os/build.gradle b/qa/os/build.gradle index 31d1bc85357f0..fc91e77606219 100644 --- a/qa/os/build.gradle +++ b/qa/os/build.gradle @@ -36,6 +36,10 @@ dependencies { compile "commons-logging:commons-logging:${versions.commonslogging}" compile project(':libs:elasticsearch-core') + + testCompile "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}" + testCompile "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" + testCompile "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}" } forbiddenApisTest { diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java index e75bfc8ad38df..163199d833ae1 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java @@ -19,10 +19,12 @@ package org.elasticsearch.packaging.test; +import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.client.fluent.Request; import org.elasticsearch.packaging.util.Distribution; import org.elasticsearch.packaging.util.Docker.DockerShell; import org.elasticsearch.packaging.util.Installation; +import org.elasticsearch.packaging.util.Platforms; import org.elasticsearch.packaging.util.ServerUtils; import org.elasticsearch.packaging.util.Shell.Result; import org.junit.After; @@ -33,13 +35,20 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import static java.nio.file.attribute.PosixFilePermissions.fromString; import static org.elasticsearch.packaging.util.Docker.assertPermissionsAndOwnership; import static org.elasticsearch.packaging.util.Docker.copyFromContainer; import static org.elasticsearch.packaging.util.Docker.ensureImageIsLoaded; import static org.elasticsearch.packaging.util.Docker.existsInContainer; +import static org.elasticsearch.packaging.util.Docker.getContainerLogs; +import static org.elasticsearch.packaging.util.Docker.getImageLabels; +import static org.elasticsearch.packaging.util.Docker.getJson; import static org.elasticsearch.packaging.util.Docker.mkDirWithPrivilegeEscalation; import static org.elasticsearch.packaging.util.Docker.removeContainer; import static org.elasticsearch.packaging.util.Docker.rmDirWithPrivilegeEscalation; @@ -54,10 +63,16 @@ import static org.elasticsearch.packaging.util.FileUtils.getTempDir; import static org.elasticsearch.packaging.util.FileUtils.rm; import static org.elasticsearch.packaging.util.ServerUtils.makeRequest; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assume.assumeTrue; public class DockerTests extends PackagingTestCase { @@ -92,14 +107,28 @@ public void teardownTest() { /** * Checks that the Docker image can be run, and that it passes various checks. */ - public void test10Install() { + public void test010Install() { verifyContainerInstallation(installation, distribution()); } + /** + * Check that the /_xpack API endpoint's presence is correct for the type of distribution being tested. + */ + public void test011PresenceOfXpack() throws Exception { + waitForElasticsearch(installation); + final int statusCode = Request.Get("http://localhost:9200/_xpack").execute().returnResponse().getStatusLine().getStatusCode(); + + if (distribution.isOSS()) { + assertThat(statusCode, greaterThanOrEqualTo(400)); + } else { + assertThat(statusCode, equalTo(200)); + } + } + /** * Checks that no plugins are initially active. */ - public void test20PluginsListWithNoPlugins() { + public void test020PluginsListWithNoPlugins() { final Installation.Executables bin = installation.executables(); final Result r = sh.run(bin.elasticsearchPlugin + " list"); @@ -109,7 +138,7 @@ public void test20PluginsListWithNoPlugins() { /** * Check that a keystore can be manually created using the provided CLI tool. */ - public void test40CreateKeystoreManually() throws InterruptedException { + public void test040CreateKeystoreManually() throws InterruptedException { final Installation.Executables bin = installation.executables(); final Path keystorePath = installation.config("elasticsearch.keystore"); @@ -125,22 +154,10 @@ public void test40CreateKeystoreManually() throws InterruptedException { assertThat(r.stdout, containsString("keystore.seed")); } - /** - * Send some basic index, count and delete requests, in order to check that the installation - * is minimally functional. - */ - public void test50BasicApiTests() throws Exception { - waitForElasticsearch(installation); - - assertTrue(existsInContainer(installation.logs.resolve("gc.log"))); - - ServerUtils.runElasticsearchTests(); - } - /** * Check that the default keystore is automatically created */ - public void test60AutoCreateKeystore() throws Exception { + public void test041AutoCreateKeystore() throws Exception { final Path keystorePath = installation.config("elasticsearch.keystore"); waitForPathToExist(keystorePath); @@ -152,10 +169,41 @@ public void test60AutoCreateKeystore() throws Exception { assertThat(result.stdout, containsString("keystore.seed")); } + /** + * Check that the JDK's cacerts file is a symlink to the copy provided by the operating system. + */ + public void test042JavaUsesTheOsProvidedKeystore() { + final String path = sh.run("realpath jdk/lib/security/cacerts").stdout; + + assertThat(path, equalTo("/etc/pki/ca-trust/extracted/java/cacerts")); + } + + /** + * Checks that there are Amazon trusted certificates in the cacaerts keystore. + */ + public void test043AmazonCaCertsAreInTheKeystore() { + final boolean matches = sh.run("jdk/bin/keytool -cacerts -storepass changeit -list | grep trustedCertEntry").stdout.lines() + .anyMatch(line -> line.contains("amazonrootca")); + + assertTrue("Expected Amazon trusted cert in cacerts", matches); + } + + /** + * Send some basic index, count and delete requests, in order to check that the installation + * is minimally functional. + */ + public void test050BasicApiTests() throws Exception { + waitForElasticsearch(installation); + + assertTrue(existsInContainer(installation.logs.resolve("gc.log"))); + + ServerUtils.runElasticsearchTests(); + } + /** * Check that the default config can be overridden using a bind mount, and that env vars are respected */ - public void test70BindMountCustomPathConfAndJvmOptions() throws Exception { + public void test070BindMountCustomPathConfAndJvmOptions() throws Exception { copyFromContainer(installation.config("elasticsearch.yml"), tempDir.resolve("elasticsearch.yml")); copyFromContainer(installation.config("log4j2.properties"), tempDir.resolve("log4j2.properties")); @@ -174,37 +222,49 @@ public void test70BindMountCustomPathConfAndJvmOptions() throws Exception { waitForElasticsearch(installation); - final String nodesResponse = makeRequest(Request.Get("http://localhost:9200/_nodes")); - assertThat(nodesResponse, containsString("\"heap_init_in_bytes\":536870912")); - assertThat(nodesResponse, containsString("\"using_compressed_ordinary_object_pointers\":\"false\"")); + final JsonNode nodes = getJson("_nodes").get("nodes"); + final String nodeId = nodes.fieldNames().next(); + + final int heapSize = nodes.at("/" + nodeId + "/jvm/mem/heap_init_in_bytes").intValue(); + final boolean usingCompressedPointers = nodes.at("/" + nodeId + "/jvm/using_compressed_ordinary_object_pointers").asBoolean(); + + logger.warn(nodes.at("/" + nodeId + "/jvm/mem/heap_init_in_bytes")); + + assertThat("heap_init_in_bytes", heapSize, equalTo(536870912)); + assertThat("using_compressed_ordinary_object_pointers", usingCompressedPointers, equalTo(false)); } /** - * Check that the default config can be overridden using a bind mount, and that env vars are respected + * Check that the default config can be overridden using a bind mount, and that env vars are respected. */ - public void test71BindMountCustomPathWithDifferentUID() throws Exception { - final Path tempEsDataDir = tempDir.resolve("esDataDir"); - // Make the local directory and contents accessible when bind-mounted - mkDirWithPrivilegeEscalation(tempEsDataDir, 1500, 0); + public void test071BindMountCustomPathWithDifferentUID() throws Exception { + Platforms.onLinux(() -> { + final Path tempEsDataDir = tempDir.resolve("esDataDir"); + // Make the local directory and contents accessible when bind-mounted + mkDirWithPrivilegeEscalation(tempEsDataDir, 1500, 0); - // Restart the container - final Map volumes = Map.of(tempEsDataDir.toAbsolutePath(), installation.data); + // Restart the container + final Map volumes = Map.of(tempEsDataDir.toAbsolutePath(), installation.data); - runContainer(distribution(), volumes, null); + runContainer(distribution(), volumes, null); - waitForElasticsearch(installation); + waitForElasticsearch(installation); - final String nodesResponse = makeRequest(Request.Get("http://localhost:9200/_nodes")); + final JsonNode nodes = getJson("_nodes"); - assertThat(nodesResponse, containsString("\"_nodes\":{\"total\":1,\"successful\":1,\"failed\":0}")); - rmDirWithPrivilegeEscalation(tempEsDataDir); + assertThat(nodes.at("/_nodes/total").intValue(), equalTo(1)); + assertThat(nodes.at("/_nodes/successful").intValue(), equalTo(1)); + assertThat(nodes.at("/_nodes/failed").intValue(), equalTo(0)); + + rmDirWithPrivilegeEscalation(tempEsDataDir); + }); } /** * Check that environment variables can be populated by setting variables with the suffix "_FILE", * which point to files that hold the required values. */ - public void test80SetEnvironmentVariablesUsingFiles() throws Exception { + public void test080SetEnvironmentVariablesUsingFiles() throws Exception { final String optionsFilename = "esJavaOpts.txt"; // ES_JAVA_OPTS_FILE @@ -231,7 +291,7 @@ public void test80SetEnvironmentVariablesUsingFiles() throws Exception { /** * Check that the elastic user's password can be configured via a file and the ELASTIC_PASSWORD_FILE environment variable. */ - public void test81ConfigurePasswordThroughEnvironmentVariableFile() throws Exception { + public void test081ConfigurePasswordThroughEnvironmentVariableFile() throws Exception { // Test relies on configuring security assumeTrue(distribution.isDefault()); @@ -241,14 +301,13 @@ public void test81ConfigurePasswordThroughEnvironmentVariableFile() throws Excep // ELASTIC_PASSWORD_FILE Files.writeString(tempDir.resolve(passwordFilename), xpackPassword + "\n"); - Map envVars = Map - .of( - "ELASTIC_PASSWORD_FILE", - "/run/secrets/" + passwordFilename, - // Enable security so that we can test that the password has been used - "xpack.security.enabled", - "true" - ); + Map envVars = Map.of( + "ELASTIC_PASSWORD_FILE", + "/run/secrets/" + passwordFilename, + // Enable security so that we can test that the password has been used + "xpack.security.enabled", + "true" + ); // File permissions need to be secured in order for the ES wrapper to accept // them for populating env var values @@ -278,15 +337,17 @@ public void test81ConfigurePasswordThroughEnvironmentVariableFile() throws Excep /** * Check that environment variables cannot be used with _FILE environment variables. */ - public void test81CannotUseEnvVarsAndFiles() throws Exception { + public void test081CannotUseEnvVarsAndFiles() throws Exception { final String optionsFilename = "esJavaOpts.txt"; // ES_JAVA_OPTS_FILE Files.writeString(tempDir.resolve(optionsFilename), "-XX:-UseCompressedOops\n"); Map envVars = Map.of( - "ES_JAVA_OPTS", "-XX:+UseCompressedOops", - "ES_JAVA_OPTS_FILE", "/run/secrets/" + optionsFilename + "ES_JAVA_OPTS", + "-XX:+UseCompressedOops", + "ES_JAVA_OPTS_FILE", + "/run/secrets/" + optionsFilename ); // File permissions need to be secured in order for the ES wrapper to accept @@ -307,7 +368,7 @@ public void test81CannotUseEnvVarsAndFiles() throws Exception { * Check that when populating environment variables by setting variables with the suffix "_FILE", * the files' permissions are checked. */ - public void test82EnvironmentVariablesUsingFilesHaveCorrectPermissions() throws Exception { + public void test082EnvironmentVariablesUsingFilesHaveCorrectPermissions() throws Exception { final String optionsFilename = "esJavaOpts.txt"; // ES_JAVA_OPTS_FILE @@ -333,7 +394,7 @@ public void test82EnvironmentVariablesUsingFilesHaveCorrectPermissions() throws * Check whether the elasticsearch-certutil tool has been shipped correctly, * and if present then it can execute. */ - public void test90SecurityCliPackaging() { + public void test090SecurityCliPackaging() { final Installation.Executables bin = installation.executables(); final Path securityCli = installation.lib.resolve("tools").resolve("security-cli"); @@ -356,7 +417,7 @@ public void test90SecurityCliPackaging() { /** * Check that the elasticsearch-shard tool is shipped in the Docker image and is executable. */ - public void test91ElasticsearchShardCliPackaging() { + public void test091ElasticsearchShardCliPackaging() { final Installation.Executables bin = installation.executables(); final Result result = sh.run(bin.elasticsearchShard + " -h"); @@ -366,10 +427,146 @@ public void test91ElasticsearchShardCliPackaging() { /** * Check that the elasticsearch-node tool is shipped in the Docker image and is executable. */ - public void test92ElasticsearchNodeCliPackaging() { + public void test092ElasticsearchNodeCliPackaging() { final Installation.Executables bin = installation.executables(); final Result result = sh.run(bin.elasticsearchNode + " -h"); - assertThat(result.stdout, containsString("A CLI tool to do unsafe cluster and index manipulations on current node")); + assertThat( + "Failed to find expected message about the elasticsearch-node CLI tool", + result.stdout, + containsString("A CLI tool to do unsafe cluster and index manipulations on current node") + ); + } + + /** + * Check that no core dumps have been accidentally included in the Docker image. + */ + public void test100NoCoreFilesInImage() { + assertFalse("Unexpected core dump found in Docker image", existsInContainer("/core*")); + } + + /** + * Check that there are no files with a GID other than 0. + */ + public void test101AllFilesAreGroupZero() { + final String findResults = sh.run("find . -not -gid 0").stdout; + + assertThat("Found some files whose GID != 0", findResults, is(emptyString())); + } + + /** + * Check that the Docker image has the expected "Label Schema" labels. + * @see Label Schema website + */ + public void test110OrgLabelSchemaLabels() throws Exception { + final Map labels = getImageLabels(distribution); + + final Map staticLabels = new HashMap<>(); + staticLabels.put("name", "Elasticsearch"); + staticLabels.put("schema-version", "1.0"); + staticLabels.put("url", "https://www.elastic.co/products/elasticsearch"); + staticLabels.put("usage", "https://www.elastic.co/guide/en/elasticsearch/reference/index.html"); + staticLabels.put("vcs-url", "https://github.com/elastic/elasticsearch"); + staticLabels.put("vendor", "Elastic"); + + if (distribution.isOSS()) { + staticLabels.put("license", "Apache-2.0"); + } else { + staticLabels.put("license", "Elastic-License"); + } + + // TODO: we should check the actual version value + final Set dynamicLabels = Set.of("build-date", "vcs-ref", "version"); + + final String prefix = "org.label-schema"; + + staticLabels.forEach((suffix, value) -> { + String key = prefix + "." + suffix; + assertThat(labels, hasKey(key)); + assertThat(labels.get(key), equalTo(value)); + }); + + dynamicLabels.forEach(label -> { + String key = prefix + "." + label; + assertThat(labels, hasKey(key)); + }); + } + + /** + * Check that the Docker image has the expected "Open Containers Annotations" labels. + * @see Open Containers Annotations + */ + public void test110OrgOpencontainersLabels() throws Exception { + final Map labels = getImageLabels(distribution); + + final Map staticLabels = new HashMap<>(); + staticLabels.put("title", "Elasticsearch"); + staticLabels.put("url", "https://www.elastic.co/products/elasticsearch"); + staticLabels.put("documentation", "https://www.elastic.co/guide/en/elasticsearch/reference/index.html"); + staticLabels.put("source", "https://github.com/elastic/elasticsearch"); + staticLabels.put("vendor", "Elastic"); + + if (distribution.isOSS()) { + staticLabels.put("licenses", "Apache-2.0"); + } else { + staticLabels.put("licenses", "Elastic-License"); + } + + // TODO: we should check the actual version value + final Set dynamicLabels = Set.of("created", "revision", "version"); + + final String prefix = "org.opencontainers.image"; + + staticLabels.forEach((suffix, value) -> { + String key = prefix + "." + suffix; + assertThat(labels, hasKey(key)); + assertThat(labels.get(key), equalTo(value)); + }); + + dynamicLabels.forEach(label -> { + String key = prefix + "." + label; + assertThat(labels, hasKey(key)); + }); + } + + /** + * Check that the container logs contain the expected content for Elasticsearch itself. + */ + public void test120DockerLogsIncludeElasticsearchLogs() throws Exception { + waitForElasticsearch(installation); + final Result containerLogs = getContainerLogs(); + + assertThat("Container logs don't contain abbreviated class names", containerLogs.stdout, containsString("o.e.n.Node")); + assertThat("Container logs don't contain INFO level messages", containerLogs.stdout, containsString("INFO")); + } + + /** + * Check that the Java process running inside the container has the expect PID, UID and username. + */ + public void test130JavaHasCorrectPidAndOwnership() { + final List processes = sh.run("ps -o pid,uid,user -C java").stdout.lines().skip(1).collect(Collectors.toList()); + + assertThat("Expected a single java process", processes, hasSize(1)); + + final String[] fields = processes.get(0).trim().split("\\s+"); + + assertThat(fields, arrayWithSize(3)); + assertThat("Incorrect PID", fields[0], equalTo("1")); + assertThat("Incorrect UID", fields[1], equalTo("1000")); + assertThat("Incorrect username", fields[2], equalTo("elasticsearch")); + } + + public void test140CgroupOsStatsAreAvailable() throws Exception { + waitForElasticsearch(installation); + + final JsonNode nodes = getJson("_nodes/stats/os").get("nodes"); + + final String nodeId = nodes.fieldNames().next(); + + final JsonNode cgroupStats = nodes.at("/" + nodeId + "/os/cgroup"); + assertFalse("Couldn't find /nodes/{nodeId}/os/cgroup in API response", cgroupStats.isMissingNode()); + + assertThat("Failed to find [cpu] in node OS cgroup stats", cgroupStats.get("cpu"), not(nullValue())); + assertThat("Failed to find [cpuacct] in node OS cgroup stats", cgroupStats.get("cpuacct"), not(nullValue())); } } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java index 8d5b7ce12c72f..95e5b586627c6 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java @@ -19,15 +19,20 @@ package org.elasticsearch.packaging.util; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.http.client.fluent.Request; import org.elasticsearch.common.CheckedRunnable; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -40,6 +45,7 @@ import static org.elasticsearch.packaging.util.FileMatcher.p770; import static org.elasticsearch.packaging.util.FileMatcher.p775; import static org.elasticsearch.packaging.util.FileUtils.getCurrentVersion; +import static org.elasticsearch.packaging.util.ServerUtils.makeRequest; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -120,7 +126,7 @@ public static Shell.Result runContainerExpectingFailure( waitForElasticsearchToExit(); - return sh.run("docker logs " + containerId); + return getContainerLogs(); } private static void executeDockerRun(Distribution distribution, Map volumes, Map envVars) { @@ -171,7 +177,7 @@ private static void waitForElasticsearchToStart() { // Give the container a chance to crash out Thread.sleep(1000); - psOutput = dockerShell.run("ps ax").stdout; + psOutput = dockerShell.run("ps -w ax").stdout; if (psOutput.contains("/usr/share/elasticsearch/jdk/bin/java")) { isElasticsearchRunning = true; @@ -183,7 +189,7 @@ private static void waitForElasticsearchToStart() { } while (attempt++ < 5); if (isElasticsearchRunning == false) { - final Shell.Result dockerLogs = sh.run("docker logs " + containerId); + final Shell.Result dockerLogs = getContainerLogs(); fail( "Elasticsearch container did not start successfully.\n\nps output:\n" + psOutput @@ -217,7 +223,7 @@ private static void waitForElasticsearchToExit() { } while (attempt++ < 5); if (isElasticsearchRunning) { - final Shell.Result dockerLogs = sh.run("docker logs " + containerId); + final Shell.Result dockerLogs = getContainerLogs(); fail("Elasticsearch container did exit.\n\nStdout:\n" + dockerLogs.stdout + "\n\nStderr:\n" + dockerLogs.stderr); } } @@ -278,6 +284,13 @@ protected String[] getScriptCommand(String script) { * Checks whether a path exists in the Docker container. */ public static boolean existsInContainer(Path path) { + return existsInContainer(path.toString()); + } + + /** + * Checks whether a path exists in the Docker container. + */ + public static boolean existsInContainer(String path) { logger.debug("Checking whether file " + path + " exists in container"); final Shell.Result result = dockerShell.runIgnoreExitCode("test -e " + path); @@ -333,9 +346,9 @@ public static void mkDirWithPrivilegeEscalation(Path localPath, int uid, int gid final PosixFileAttributes dirAttributes = FileUtils.getPosixFileAttributes(localPath); final Map numericPathOwnership = FileUtils.getNumericUnixPathOwnership(localPath); - assertEquals(localPath + " has wrong uid", numericPathOwnership.get("uid").intValue(), uid); - assertEquals(localPath + " has wrong gid", numericPathOwnership.get("gid").intValue(), gid); - assertEquals(localPath + " has wrong permissions", dirAttributes.permissions(), p770); + assertThat(localPath + " has wrong uid", numericPathOwnership.get("uid"), equalTo(uid)); + assertThat(localPath + " has wrong gid", numericPathOwnership.get("gid"), equalTo(gid)); + assertThat(localPath + " has wrong permissions", dirAttributes.permissions(), equalTo(p770)); } /** @@ -414,63 +427,47 @@ private static void verifyOssInstallation(Installation es) { Stream.of(es.plugins, es.modules).forEach(dir -> assertPermissionsAndOwnership(dir, p755)); - // FIXME these files should all have the same permissions - Stream - .of( - "elasticsearch.keystore", - // "elasticsearch.yml", - "jvm.options" - // "log4j2.properties" - ) + Stream.of("elasticsearch.keystore", "elasticsearch.yml", "jvm.options", "log4j2.properties") .forEach(configFile -> assertPermissionsAndOwnership(es.config(configFile), p660)); - Stream - .of("elasticsearch.yml", "log4j2.properties") - .forEach(configFile -> assertPermissionsAndOwnership(es.config(configFile), p644)); - assertThat(dockerShell.run(es.bin("elasticsearch-keystore") + " list").stdout, containsString("keystore.seed")); Stream.of(es.bin, es.lib).forEach(dir -> assertPermissionsAndOwnership(dir, p755)); - Stream - .of( - "elasticsearch", - "elasticsearch-cli", - "elasticsearch-env", - "elasticsearch-enve", - "elasticsearch-keystore", - "elasticsearch-node", - "elasticsearch-plugin", - "elasticsearch-shard" - ) - .forEach(executable -> assertPermissionsAndOwnership(es.bin(executable), p755)); + Stream.of( + "elasticsearch", + "elasticsearch-cli", + "elasticsearch-env", + "elasticsearch-enve", + "elasticsearch-keystore", + "elasticsearch-node", + "elasticsearch-plugin", + "elasticsearch-shard" + ).forEach(executable -> assertPermissionsAndOwnership(es.bin(executable), p755)); Stream.of("LICENSE.txt", "NOTICE.txt", "README.textile").forEach(doc -> assertPermissionsAndOwnership(es.home.resolve(doc), p644)); } private static void verifyDefaultInstallation(Installation es) { - Stream - .of( - "elasticsearch-certgen", - "elasticsearch-certutil", - "elasticsearch-croneval", - "elasticsearch-saml-metadata", - "elasticsearch-setup-passwords", - "elasticsearch-sql-cli", - "elasticsearch-syskeygen", - "elasticsearch-users", - "x-pack-env", - "x-pack-security-env", - "x-pack-watcher-env" - ) - .forEach(executable -> assertPermissionsAndOwnership(es.bin(executable), p755)); + Stream.of( + "elasticsearch-certgen", + "elasticsearch-certutil", + "elasticsearch-croneval", + "elasticsearch-saml-metadata", + "elasticsearch-setup-passwords", + "elasticsearch-sql-cli", + "elasticsearch-syskeygen", + "elasticsearch-users", + "x-pack-env", + "x-pack-security-env", + "x-pack-watcher-env" + ).forEach(executable -> assertPermissionsAndOwnership(es.bin(executable), p755)); // at this time we only install the current version of archive distributions, but if that changes we'll need to pass // the version through here assertPermissionsAndOwnership(es.bin("elasticsearch-sql-cli-" + getCurrentVersion() + ".jar"), p755); - Stream - .of("role_mapping.yml", "roles.yml", "users", "users_roles") + Stream.of("role_mapping.yml", "roles.yml", "users", "users_roles") .forEach(configFile -> assertPermissionsAndOwnership(es.config(configFile), p660)); } @@ -483,13 +480,46 @@ public static void waitForElasticsearch(String status, String index, Installatio withLogging(() -> ServerUtils.waitForElasticsearch(status, index, installation, username, password)); } + /** + * Runs the provided closure, and captures logging information if an exception is thrown. + * @param r the closure to run + * @throws Exception any exception encountered while running the closure are propagated. + */ private static void withLogging(CheckedRunnable r) throws Exception { try { r.run(); } catch (Exception e) { - final Shell.Result logs = sh.run("docker logs " + containerId); + final Shell.Result logs = getContainerLogs(); logger.warn("Elasticsearch container failed to start.\n\nStdout:\n" + logs.stdout + "\n\nStderr:\n" + logs.stderr); throw e; } } + + public static JsonNode getJson(String path) throws IOException { + final String pluginsResponse = makeRequest(Request.Get("http://localhost:9200/" + path)); + + ObjectMapper mapper = new ObjectMapper(); + + return mapper.readTree(pluginsResponse); + } + + public static Map getImageLabels(Distribution distribution) throws Exception { + // The format below extracts the .Config.Labels value, and prints it as json. Without the json + // modifier, a stringified Go map is printed instead, which isn't helpful. + String labelsJson = sh.run("docker inspect -f '{{json .Config.Labels}}' " + distribution.flavor.name + ":test").stdout; + + ObjectMapper mapper = new ObjectMapper(); + + final JsonNode jsonNode = mapper.readTree(labelsJson); + + Map labels = new HashMap<>(); + + jsonNode.fieldNames().forEachRemaining(field -> labels.put(field, jsonNode.get(field).asText())); + + return labels; + } + + public static Shell.Result getContainerLogs() { + return sh.run("docker logs " + containerId); + } } From 2aa8c89ca9c59dd904d100685d4a3f8224008df3 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Tue, 3 Dec 2019 12:41:32 +0000 Subject: [PATCH 052/686] Silence lint warnings in server project - part 2 (#49728) Part of #40366. Silence a number of lint warnings in the :server project, which arise when re-enabling suppressed warnings in server/build.gradle. --- .../search/grouping/CollapseTopFieldDocs.java | 2 ++ .../admin/indices/rollover/RolloverInfo.java | 1 + .../action/explain/ExplainResponse.java | 10 ++++++++-- .../support/broadcast/BroadcastResponse.java | 1 + .../termvectors/MultiTermVectorsRequest.java | 1 + .../elasticsearch/client/node/NodeClient.java | 2 ++ .../elasticsearch/cluster/DiffableUtils.java | 13 +++++++++---- .../cluster/metadata/MetaData.java | 1 + .../cluster/routing/RoutingTable.java | 1 + .../common/collect/ImmutableOpenIntMap.java | 5 +++-- .../common/collect/ImmutableOpenMap.java | 5 +++-- .../common/xcontent/XContentHelper.java | 17 ++++++++--------- .../elasticsearch/index/mapper/TypeParsers.java | 3 ++- .../org/elasticsearch/plugins/SearchPlugin.java | 1 + .../AbstractPipelineAggregationBuilder.java | 3 ++- .../search/builder/SearchSourceBuilder.java | 4 ++++ .../search/fetch/StoredFieldsContext.java | 1 + .../search/internal/ContextIndexSearcher.java | 1 + .../search/slice/SliceBuilder.java | 1 + .../elasticsearch/search/suggest/Suggest.java | 2 +- .../search/suggest/SuggestionSearchContext.java | 1 + .../transport/TransportMessageListener.java | 1 + 22 files changed, 55 insertions(+), 22 deletions(-) diff --git a/server/src/main/java/org/apache/lucene/search/grouping/CollapseTopFieldDocs.java b/server/src/main/java/org/apache/lucene/search/grouping/CollapseTopFieldDocs.java index 4dba67abdeb9a..93d9d11a63a06 100644 --- a/server/src/main/java/org/apache/lucene/search/grouping/CollapseTopFieldDocs.java +++ b/server/src/main/java/org/apache/lucene/search/grouping/CollapseTopFieldDocs.java @@ -103,6 +103,7 @@ static boolean tieBreakLessThan(ShardRef first, ScoreDoc firstDoc, ShardRef seco } } + @SuppressWarnings("rawtypes") private static class MergeSortQueue extends PriorityQueue { // These are really FieldDoc instances: final ScoreDoc[][] shardHits; @@ -137,6 +138,7 @@ private static class MergeSortQueue extends PriorityQueue { // Returns true if first is < second @Override + @SuppressWarnings({"rawtypes", "unchecked"}) public boolean lessThan(ShardRef first, ShardRef second) { assert first != second; final FieldDoc firstFD = (FieldDoc) shardHits[first.shardIndex][first.hitIndex]; diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/RolloverInfo.java b/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/RolloverInfo.java index af593481e8a6d..edb8e3b16e7f4 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/RolloverInfo.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/RolloverInfo.java @@ -62,6 +62,7 @@ public RolloverInfo(String alias, List> metConditions, long time) { this.time = time; } + @SuppressWarnings("unchecked") public RolloverInfo(StreamInput in) throws IOException { this.alias = in.readString(); this.time = in.readVLong(); diff --git a/server/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java b/server/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java index 6f85ab2a5dac0..ca398554c50c0 100644 --- a/server/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java +++ b/server/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java @@ -154,6 +154,13 @@ public void writeTo(StreamOutput out) throws IOException { static { PARSER.declareString(ConstructingObjectParser.constructorArg(), _INDEX); PARSER.declareString(ConstructingObjectParser.constructorArg(), _ID); + final ConstructingObjectParser explanationParser = getExplanationsParser(); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), explanationParser, EXPLANATION); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> GetResult.fromXContentEmbedded(p), GET); + } + + @SuppressWarnings("unchecked") + private static ConstructingObjectParser getExplanationsParser() { final ConstructingObjectParser explanationParser = new ConstructingObjectParser<>("explanation", true, arg -> { if ((float) arg[0] > 0) { @@ -165,8 +172,7 @@ public void writeTo(StreamOutput out) throws IOException { explanationParser.declareFloat(ConstructingObjectParser.constructorArg(), VALUE); explanationParser.declareString(ConstructingObjectParser.constructorArg(), DESCRIPTION); explanationParser.declareObjectArray(ConstructingObjectParser.constructorArg(), explanationParser, DETAILS); - PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), explanationParser, EXPLANATION); - PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> GetResult.fromXContentEmbedded(p), GET); + return explanationParser; } public static ExplainResponse fromXContent(XContentParser parser, boolean exists) { diff --git a/server/src/main/java/org/elasticsearch/action/support/broadcast/BroadcastResponse.java b/server/src/main/java/org/elasticsearch/action/support/broadcast/BroadcastResponse.java index 9f5474c916d74..e2c8a92f0ae8a 100644 --- a/server/src/main/java/org/elasticsearch/action/support/broadcast/BroadcastResponse.java +++ b/server/src/main/java/org/elasticsearch/action/support/broadcast/BroadcastResponse.java @@ -55,6 +55,7 @@ public class BroadcastResponse extends ActionResponse implements ToXContentObjec private int failedShards; private DefaultShardOperationFailedException[] shardFailures = EMPTY; + @SuppressWarnings("unchecked") protected static void declareBroadcastFields(ConstructingObjectParser PARSER) { ConstructingObjectParser shardsParser = new ConstructingObjectParser<>("_shards", true, arg -> new BroadcastResponse((int) arg[0], (int) arg[1], (int) arg[2], (List) arg[3])); diff --git a/server/src/main/java/org/elasticsearch/action/termvectors/MultiTermVectorsRequest.java b/server/src/main/java/org/elasticsearch/action/termvectors/MultiTermVectorsRequest.java index 954e540e913a9..056947484a0f5 100644 --- a/server/src/main/java/org/elasticsearch/action/termvectors/MultiTermVectorsRequest.java +++ b/server/src/main/java/org/elasticsearch/action/termvectors/MultiTermVectorsRequest.java @@ -165,6 +165,7 @@ public int size() { } @Override + @SuppressWarnings("unchecked") public MultiTermVectorsRequest realtime(boolean realtime) { for (TermVectorsRequest request : requests) { request.realtime(realtime); diff --git a/server/src/main/java/org/elasticsearch/client/node/NodeClient.java b/server/src/main/java/org/elasticsearch/client/node/NodeClient.java index f28beb847bfb6..40bbf81534b58 100644 --- a/server/src/main/java/org/elasticsearch/client/node/NodeClient.java +++ b/server/src/main/java/org/elasticsearch/client/node/NodeClient.java @@ -48,6 +48,7 @@ */ public class NodeClient extends AbstractClient { + @SuppressWarnings("rawtypes") private Map actions; private TaskManager taskManager; @@ -63,6 +64,7 @@ public NodeClient(Settings settings, ThreadPool threadPool) { super(settings, threadPool); } + @SuppressWarnings("rawtypes") public void initialize(Map actions, TaskManager taskManager, Supplier localNodeId, RemoteClusterService remoteClusterService) { this.actions = actions; diff --git a/server/src/main/java/org/elasticsearch/cluster/DiffableUtils.java b/server/src/main/java/org/elasticsearch/cluster/DiffableUtils.java index 112856f7490d0..65f52112cf6ce 100644 --- a/server/src/main/java/org/elasticsearch/cluster/DiffableUtils.java +++ b/server/src/main/java/org/elasticsearch/cluster/DiffableUtils.java @@ -609,18 +609,20 @@ default boolean supportsVersion(V value, Version version) { * @param type of map values */ public abstract static class DiffableValueSerializer> implements ValueSerializer { + @SuppressWarnings("rawtypes") private static final DiffableValueSerializer WRITE_ONLY_INSTANCE = new DiffableValueSerializer() { @Override - public Object read(StreamInput in, Object key) throws IOException { + public Object read(StreamInput in, Object key) { throw new UnsupportedOperationException(); } @Override - public Diff readDiff(StreamInput in, Object key) throws IOException { + public Diff readDiff(StreamInput in, Object key) { throw new UnsupportedOperationException(); } }; + @SuppressWarnings("unchecked") private static > DiffableValueSerializer getWriteOnlyInstance() { return WRITE_ONLY_INSTANCE; } @@ -640,6 +642,7 @@ public void write(V value, StreamOutput out) throws IOException { value.writeTo(out); } + @Override public void writeDiff(Diff value, StreamOutput out) throws IOException { value.writeTo(out); } @@ -663,12 +666,12 @@ public Diff diff(V value, V beforePart) { } @Override - public void writeDiff(Diff value, StreamOutput out) throws IOException { + public void writeDiff(Diff value, StreamOutput out) { throw new UnsupportedOperationException(); } @Override - public Diff readDiff(StreamInput in, K key) throws IOException { + public Diff readDiff(StreamInput in, K key) { throw new UnsupportedOperationException(); } } @@ -703,9 +706,11 @@ public Diff readDiff(StreamInput in, K key) throws IOException { * * @param type of map key */ + @SuppressWarnings("rawtypes") public static class StringSetValueSerializer extends NonDiffableValueSerializer> { private static final StringSetValueSerializer INSTANCE = new StringSetValueSerializer(); + @SuppressWarnings("unchecked") public static StringSetValueSerializer getInstance() { return INSTANCE; } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java index a6ad4e2d19cb7..ce67e5b72f1d1 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java @@ -642,6 +642,7 @@ public IndexGraveyard indexGraveyard() { return custom(IndexGraveyard.TYPE); } + @SuppressWarnings("unchecked") public T custom(String type) { return (T) customs.get(type); } diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java b/server/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java index 3a49577563929..df5e32f9f1708 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java @@ -410,6 +410,7 @@ public Builder(RoutingTable routingTable) { } } + @SuppressWarnings("unchecked") public Builder updateNodes(long version, RoutingNodes routingNodes) { // this is being called without pre initializing the routing table, so we must copy over the version as well this.version = version; diff --git a/server/src/main/java/org/elasticsearch/common/collect/ImmutableOpenIntMap.java b/server/src/main/java/org/elasticsearch/common/collect/ImmutableOpenIntMap.java index cb4457ce24b9b..74ee39db1f967 100644 --- a/server/src/main/java/org/elasticsearch/common/collect/ImmutableOpenIntMap.java +++ b/server/src/main/java/org/elasticsearch/common/collect/ImmutableOpenIntMap.java @@ -174,6 +174,7 @@ public String toString() { } @Override + @SuppressWarnings("rawtypes") public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; @@ -190,7 +191,7 @@ public int hashCode() { return map.hashCode(); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"rawtypes", "unchecked"}) private static final ImmutableOpenIntMap EMPTY = new ImmutableOpenIntMap(new IntObjectHashMap()); @SuppressWarnings("unchecked") @@ -214,8 +215,8 @@ public static class Builder implements IntObjectMap { private IntObjectHashMap map; + @SuppressWarnings("unchecked") public Builder() { - //noinspection unchecked this(EMPTY); } diff --git a/server/src/main/java/org/elasticsearch/common/collect/ImmutableOpenMap.java b/server/src/main/java/org/elasticsearch/common/collect/ImmutableOpenMap.java index 85178283dfabf..5c16e66a1d953 100644 --- a/server/src/main/java/org/elasticsearch/common/collect/ImmutableOpenMap.java +++ b/server/src/main/java/org/elasticsearch/common/collect/ImmutableOpenMap.java @@ -176,6 +176,7 @@ public String toString() { } @Override + @SuppressWarnings("rawtypes") public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; @@ -192,7 +193,7 @@ public int hashCode() { return map.hashCode(); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private static final ImmutableOpenMap EMPTY = new ImmutableOpenMap(new ObjectObjectHashMap()); @SuppressWarnings("unchecked") @@ -224,8 +225,8 @@ public static Builder builder(ImmutableOpenMap implements ObjectObjectMap { private ObjectObjectHashMap map; + @SuppressWarnings("unchecked") public Builder() { - //noinspection unchecked this(EMPTY); } diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java b/server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java index cc3158b631660..4015be991a4b2 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java @@ -250,10 +250,9 @@ public static void mergeDefaults(Map content, Map) content.get(defaultEntry.getKey()), (Map) defaultEntry.getValue()); } else if (content.get(defaultEntry.getKey()) instanceof List && defaultEntry.getValue() instanceof List) { - List defaultList = (List) defaultEntry.getValue(); - List contentList = (List) content.get(defaultEntry.getKey()); + List defaultList = (List) defaultEntry.getValue(); + List contentList = (List) content.get(defaultEntry.getKey()); - List mergedList = new ArrayList(); if (allListValuesAreMapsOfOne(defaultList) && allListValuesAreMapsOfOne(contentList)) { // all are in the form of [ {"key1" : {}}, {"key2" : {}} ], merge based on keys Map> processed = new LinkedHashMap<>(); @@ -272,26 +271,26 @@ public static void mergeDefaults(Map content, Map map : processed.values()) { - mergedList.add(map); - } + + content.put(defaultEntry.getKey(), new ArrayList<>(processed.values())); } else { // if both are lists, simply combine them, first the defaults, then the content // just make sure not to add the same value twice - mergedList.addAll(defaultList); + List mergedList = new ArrayList<>(defaultList); + for (Object o : contentList) { if (!mergedList.contains(o)) { mergedList.add(o); } } + content.put(defaultEntry.getKey(), mergedList); } - content.put(defaultEntry.getKey(), mergedList); } } } } - private static boolean allListValuesAreMapsOfOne(List list) { + private static boolean allListValuesAreMapsOfOne(List list) { for (Object o : list) { if (!(o instanceof Map)) { return false; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java b/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java index 0e4f0510b0614..cabadedcd7f20 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java @@ -152,6 +152,7 @@ public static void parseNorms(FieldMapper.Builder builder, String fieldName, Obj * Parse text field attributes. In addition to {@link #parseField common attributes} * this will parse analysis and term-vectors related settings. */ + @SuppressWarnings("unchecked") public static void parseTextField(FieldMapper.Builder builder, String name, Map fieldNode, Mapper.TypeParser.ParserContext parserContext) { parseField(builder, name, fieldNode, parserContext); @@ -217,7 +218,7 @@ public static void parseField(FieldMapper.Builder builder, String name, Map getSuggestionReader() { return this.suggestionReader; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/AbstractPipelineAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/AbstractPipelineAggregationBuilder.java index fa2f8c8090bcd..35eab881bb643 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/AbstractPipelineAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/AbstractPipelineAggregationBuilder.java @@ -105,7 +105,7 @@ public final PipelineAggregator create() { public void doValidate(AggregatorFactory parent, Collection factories, Collection pipelineAggregatorFactories) { } - + /** * Validates pipeline aggregations that need sequentially ordered data. */ @@ -175,6 +175,7 @@ public int hashCode() { } @Override + @SuppressWarnings("unchecked") public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index bb3dd945db95a..857bc235a40ff 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -177,6 +177,7 @@ public static HighlightBuilder highlight() { private SuggestBuilder suggestBuilder; + @SuppressWarnings("rawtypes") private List rescoreBuilders; private List indexBoosts = new ArrayList<>(); @@ -692,6 +693,7 @@ public boolean profile() { /** * Gets the bytes representing the rescore builders for this request. */ + @SuppressWarnings("rawtypes") public List rescores() { return rescoreBuilders; } @@ -921,6 +923,7 @@ public boolean isSuggestOnly() { * infinitely. */ @Override + @SuppressWarnings({"unchecked", "rawtypes"}) public SearchSourceBuilder rewrite(QueryRewriteContext context) throws IOException { assert (this.equals(shallowCopy(queryBuilder, postQueryBuilder, aggregations, sliceBuilder, sorts, rescoreBuilders, highlightBuilder))); @@ -964,6 +967,7 @@ public SearchSourceBuilder copyWithNewSlice(SliceBuilder slice) { * Create a shallow copy of this source replaced {@link #queryBuilder}, {@link #postQueryBuilder}, and {@link #sliceBuilder}. Used by * {@link #rewrite(QueryRewriteContext)} and {@link #copyWithNewSlice(SliceBuilder)}. */ + @SuppressWarnings("rawtypes") private SearchSourceBuilder shallowCopy(QueryBuilder queryBuilder, QueryBuilder postQueryBuilder, AggregatorFactories.Builder aggregations, SliceBuilder slice, List> sorts, List rescoreBuilders, HighlightBuilder highlightBuilder) { diff --git a/server/src/main/java/org/elasticsearch/search/fetch/StoredFieldsContext.java b/server/src/main/java/org/elasticsearch/search/fetch/StoredFieldsContext.java index 9ac90806aa4c7..be72cac298f86 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/StoredFieldsContext.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/StoredFieldsContext.java @@ -64,6 +64,7 @@ public StoredFieldsContext(StoredFieldsContext other) { } } + @SuppressWarnings("unchecked") public StoredFieldsContext(StreamInput in) throws IOException { this.fetchFields = in.readBoolean(); if (fetchFields) { diff --git a/server/src/main/java/org/elasticsearch/search/internal/ContextIndexSearcher.java b/server/src/main/java/org/elasticsearch/search/internal/ContextIndexSearcher.java index 85a0010fd58f9..8959280ad21a1 100644 --- a/server/src/main/java/org/elasticsearch/search/internal/ContextIndexSearcher.java +++ b/server/src/main/java/org/elasticsearch/search/internal/ContextIndexSearcher.java @@ -145,6 +145,7 @@ private void checkCancelled() { } } + @SuppressWarnings({"unchecked", "rawtypes"}) public void search(List leaves, Weight weight, CollectorManager manager, QuerySearchResult result, DocValueFormat[] formats, TotalHits totalHits) throws IOException { final List collectors = new ArrayList<>(leaves.size()); diff --git a/server/src/main/java/org/elasticsearch/search/slice/SliceBuilder.java b/server/src/main/java/org/elasticsearch/search/slice/SliceBuilder.java index 21995c663b66c..fe940ff94fdff 100644 --- a/server/src/main/java/org/elasticsearch/search/slice/SliceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/slice/SliceBuilder.java @@ -204,6 +204,7 @@ public int hashCode() { * * @param context Additional information needed to build the query */ + @SuppressWarnings("rawtypes") public Query toFilter(ClusterService clusterService, ShardSearchRequest request, QueryShardContext context) { final MappedFieldType type = context.fieldMapper(field); if (type == null) { diff --git a/server/src/main/java/org/elasticsearch/search/suggest/Suggest.java b/server/src/main/java/org/elasticsearch/search/suggest/Suggest.java index 0d7e32e83dd1f..9b8d814a7b9f5 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/Suggest.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/Suggest.java @@ -190,7 +190,7 @@ public static List>> reduce(MapsuggestionType contained in this {@link Suggest} instance */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"rawtypes", "unchecked"}) public List filter(Class suggestionType) { return suggestions.stream() .filter(suggestion -> suggestion.getClass() == suggestionType) diff --git a/server/src/main/java/org/elasticsearch/search/suggest/SuggestionSearchContext.java b/server/src/main/java/org/elasticsearch/search/suggest/SuggestionSearchContext.java index 165f70ba3c0c6..59a39967c31a9 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/SuggestionSearchContext.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/SuggestionSearchContext.java @@ -78,6 +78,7 @@ public void setRegex(BytesRef regex) { this.regex = regex; } + @SuppressWarnings("unchecked") public Suggester getSuggester() { return ((Suggester) suggester); } diff --git a/server/src/main/java/org/elasticsearch/transport/TransportMessageListener.java b/server/src/main/java/org/elasticsearch/transport/TransportMessageListener.java index 62ff3d8fa4302..c9d72a43f8690 100644 --- a/server/src/main/java/org/elasticsearch/transport/TransportMessageListener.java +++ b/server/src/main/java/org/elasticsearch/transport/TransportMessageListener.java @@ -64,5 +64,6 @@ default void onRequestSent(DiscoveryNode node, long requestId, String action, Tr * @param requestId the request id for this reponse * @param context the response context or null if the context was already processed ie. due to a timeout. */ + @SuppressWarnings("rawtypes") default void onResponseReceived(long requestId, Transport.ResponseContext context) {} } From cd900888d12ca22aecac1d8b673fd92b951eb230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Tue, 3 Dec 2019 15:04:14 +0100 Subject: [PATCH 053/686] A few cleanups in evaluation tests (#49791) --- .../client/MachineLearningIT.java | 54 +++++++---- .../ml/EvaluateDataFrameResponseTests.java | 54 +++++++---- .../AccuracyMetricResultTests.java | 14 ++- .../classification/ClassificationTests.java | 5 +- ...classConfusionMatrixMetricResultTests.java | 8 +- .../AucRocMetricAucRocPointTests.java | 3 +- .../AucRocMetricResultTests.java | 7 +- ...usionMatrixMetricConfusionMatrixTests.java | 3 +- .../ConfusionMatrixMetricResultTests.java | 7 +- .../PrecisionMetricResultTests.java | 5 +- .../RecallMetricResultTests.java | 5 +- .../evaluation/MockAggregations.java | 96 +++++++++++++++++++ .../classification/AccuracyTests.java | 41 +++----- .../classification/ClassificationTests.java | 5 +- .../MulticlassConfusionMatrixTests.java | 57 ++--------- .../regression/MeanSquaredErrorTests.java | 17 +--- .../evaluation/regression/RSquaredTests.java | 55 ++++------- .../ConfusionMatrixTests.java | 27 ++---- .../softclassification/PrecisionTests.java | 27 ++---- .../softclassification/RecallTests.java | 27 ++---- 20 files changed, 276 insertions(+), 241 deletions(-) rename client/rest-high-level/src/test/java/org/elasticsearch/client/ml/{ => dataframe/evaluation/softclassification}/AucRocMetricAucRocPointTests.java (92%) rename client/rest-high-level/src/test/java/org/elasticsearch/client/ml/{ => dataframe/evaluation/softclassification}/AucRocMetricResultTests.java (88%) rename client/rest-high-level/src/test/java/org/elasticsearch/client/ml/{ => dataframe/evaluation/softclassification}/ConfusionMatrixMetricConfusionMatrixTests.java (92%) rename client/rest-high-level/src/test/java/org/elasticsearch/client/ml/{ => dataframe/evaluation/softclassification}/ConfusionMatrixMetricResultTests.java (87%) rename client/rest-high-level/src/test/java/org/elasticsearch/client/ml/{ => dataframe/evaluation/softclassification}/PrecisionMetricResultTests.java (91%) rename client/rest-high-level/src/test/java/org/elasticsearch/client/ml/{ => dataframe/evaluation/softclassification}/RecallMetricResultTests.java (91%) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MockAggregations.java diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index 74c7ce0a6cda0..6ed3734831aa2 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -33,9 +33,6 @@ import org.elasticsearch.client.indices.GetIndexRequest; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; -import org.elasticsearch.client.ml.DeleteTrainedModelRequest; -import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsRequest; -import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsResponse; import org.elasticsearch.client.ml.DeleteCalendarEventRequest; import org.elasticsearch.client.ml.DeleteCalendarJobRequest; import org.elasticsearch.client.ml.DeleteCalendarRequest; @@ -48,8 +45,11 @@ import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.DeleteModelSnapshotRequest; +import org.elasticsearch.client.ml.DeleteTrainedModelRequest; import org.elasticsearch.client.ml.EvaluateDataFrameRequest; import org.elasticsearch.client.ml.EvaluateDataFrameResponse; +import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsRequest; +import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsResponse; import org.elasticsearch.client.ml.FindFileStructureRequest; import org.elasticsearch.client.ml.FindFileStructureResponse; import org.elasticsearch.client.ml.FlushJobRequest; @@ -135,8 +135,6 @@ import org.elasticsearch.client.ml.dataframe.evaluation.classification.AccuracyMetric; import org.elasticsearch.client.ml.dataframe.evaluation.classification.Classification; import org.elasticsearch.client.ml.dataframe.evaluation.classification.MulticlassConfusionMatrixMetric; -import org.elasticsearch.client.ml.dataframe.evaluation.classification.MulticlassConfusionMatrixMetric.ActualClass; -import org.elasticsearch.client.ml.dataframe.evaluation.classification.MulticlassConfusionMatrixMetric.PredictedClass; import org.elasticsearch.client.ml.dataframe.evaluation.regression.MeanSquaredErrorMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.RSquaredMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.Regression; @@ -1822,9 +1820,12 @@ public void testEvaluateDataFrame_Classification() throws IOException { accuracyResult.getActualClasses(), equalTo( List.of( - new AccuracyMetric.ActualClass("cat", 5, 0.6), // 3 out of 5 examples labeled as "cat" were classified correctly - new AccuracyMetric.ActualClass("dog", 4, 0.75), // 3 out of 4 examples labeled as "dog" were classified correctly - new AccuracyMetric.ActualClass("ant", 1, 0.0)))); // no examples labeled as "ant" were classified correctly + // 3 out of 5 examples labeled as "cat" were classified correctly + new AccuracyMetric.ActualClass("cat", 5, 0.6), + // 3 out of 4 examples labeled as "dog" were classified correctly + new AccuracyMetric.ActualClass("dog", 4, 0.75), + // no examples labeled as "ant" were classified correctly + new AccuracyMetric.ActualClass("ant", 1, 0.0)))); assertThat(accuracyResult.getOverallAccuracy(), equalTo(0.6)); // 6 out of 10 examples were classified correctly } { // No size provided for MulticlassConfusionMatrixMetric, default used instead @@ -1846,20 +1847,29 @@ public void testEvaluateDataFrame_Classification() throws IOException { mcmResult.getConfusionMatrix(), equalTo( List.of( - new ActualClass( + new MulticlassConfusionMatrixMetric.ActualClass( "ant", 1L, - List.of(new PredictedClass("ant", 0L), new PredictedClass("cat", 1L), new PredictedClass("dog", 0L)), + List.of( + new MulticlassConfusionMatrixMetric.PredictedClass("ant", 0L), + new MulticlassConfusionMatrixMetric.PredictedClass("cat", 1L), + new MulticlassConfusionMatrixMetric.PredictedClass("dog", 0L)), 0L), - new ActualClass( + new MulticlassConfusionMatrixMetric.ActualClass( "cat", 5L, - List.of(new PredictedClass("ant", 0L), new PredictedClass("cat", 3L), new PredictedClass("dog", 1L)), + List.of( + new MulticlassConfusionMatrixMetric.PredictedClass("ant", 0L), + new MulticlassConfusionMatrixMetric.PredictedClass("cat", 3L), + new MulticlassConfusionMatrixMetric.PredictedClass("dog", 1L)), 1L), - new ActualClass( + new MulticlassConfusionMatrixMetric.ActualClass( "dog", 4L, - List.of(new PredictedClass("ant", 0L), new PredictedClass("cat", 1L), new PredictedClass("dog", 3L)), + List.of( + new MulticlassConfusionMatrixMetric.PredictedClass("ant", 0L), + new MulticlassConfusionMatrixMetric.PredictedClass("cat", 1L), + new MulticlassConfusionMatrixMetric.PredictedClass("dog", 3L)), 0L)))); assertThat(mcmResult.getOtherActualClassCount(), equalTo(0L)); } @@ -1882,8 +1892,20 @@ public void testEvaluateDataFrame_Classification() throws IOException { mcmResult.getConfusionMatrix(), equalTo( List.of( - new ActualClass("cat", 5L, List.of(new PredictedClass("cat", 3L), new PredictedClass("dog", 1L)), 1L), - new ActualClass("dog", 4L, List.of(new PredictedClass("cat", 1L), new PredictedClass("dog", 3L)), 0L) + new MulticlassConfusionMatrixMetric.ActualClass( + "cat", + 5L, + List.of( + new MulticlassConfusionMatrixMetric.PredictedClass("cat", 3L), + new MulticlassConfusionMatrixMetric.PredictedClass("dog", 1L)), + 1L), + new MulticlassConfusionMatrixMetric.ActualClass( + "dog", + 4L, + List.of( + new MulticlassConfusionMatrixMetric.PredictedClass("cat", 1L), + new MulticlassConfusionMatrixMetric.PredictedClass("dog", 3L)), + 0L) ))); assertThat(mcmResult.getOtherActualClassCount(), equalTo(1L)); } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/EvaluateDataFrameResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/EvaluateDataFrameResponseTests.java index 70740a3268f10..f6b7459b1043b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/EvaluateDataFrameResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/EvaluateDataFrameResponseTests.java @@ -20,36 +20,56 @@ import org.elasticsearch.client.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; +import org.elasticsearch.client.ml.dataframe.evaluation.classification.AccuracyMetricResultTests; +import org.elasticsearch.client.ml.dataframe.evaluation.classification.Classification; +import org.elasticsearch.client.ml.dataframe.evaluation.classification.MulticlassConfusionMatrixMetricResultTests; import org.elasticsearch.client.ml.dataframe.evaluation.regression.MeanSquaredErrorMetricResultTests; +import org.elasticsearch.client.ml.dataframe.evaluation.regression.RSquaredMetricResultTests; +import org.elasticsearch.client.ml.dataframe.evaluation.regression.Regression; +import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.AucRocMetricResultTests; +import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.BinarySoftClassification; +import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.ConfusionMatrixMetricResultTests; +import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.PrecisionMetricResultTests; +import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.RecallMetricResultTests; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Predicate; public class EvaluateDataFrameResponseTests extends AbstractXContentTestCase { public static EvaluateDataFrameResponse randomResponse() { - List metrics = new ArrayList<>(); - if (randomBoolean()) { - metrics.add(AucRocMetricResultTests.randomResult()); + String evaluationName = randomFrom(BinarySoftClassification.NAME, Classification.NAME, Regression.NAME); + List metrics; + switch (evaluationName) { + case BinarySoftClassification.NAME: + metrics = randomSubsetOf( + Arrays.asList( + AucRocMetricResultTests.randomResult(), + PrecisionMetricResultTests.randomResult(), + RecallMetricResultTests.randomResult(), + ConfusionMatrixMetricResultTests.randomResult())); + break; + case Regression.NAME: + metrics = randomSubsetOf( + Arrays.asList( + MeanSquaredErrorMetricResultTests.randomResult(), + RSquaredMetricResultTests.randomResult())); + break; + case Classification.NAME: + metrics = randomSubsetOf( + Arrays.asList( + AccuracyMetricResultTests.randomResult(), + MulticlassConfusionMatrixMetricResultTests.randomResult())); + break; + default: + throw new AssertionError("Please add missing \"case\" variant to the \"switch\" statement"); } - if (randomBoolean()) { - metrics.add(PrecisionMetricResultTests.randomResult()); - } - if (randomBoolean()) { - metrics.add(RecallMetricResultTests.randomResult()); - } - if (randomBoolean()) { - metrics.add(ConfusionMatrixMetricResultTests.randomResult()); - } - if (randomBoolean()) { - metrics.add(MeanSquaredErrorMetricResultTests.randomResult()); - } - return new EvaluateDataFrameResponse(randomAlphaOfLength(5), metrics); + return new EvaluateDataFrameResponse(evaluationName, metrics); } @Override diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetricResultTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetricResultTests.java index 4e6557b4f58bf..df48ef3123dd1 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetricResultTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetricResultTests.java @@ -31,15 +31,14 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -public class AccuracyMetricResultTests extends AbstractXContentTestCase { +public class AccuracyMetricResultTests extends AbstractXContentTestCase { @Override protected NamedXContentRegistry xContentRegistry() { return new NamedXContentRegistry(new MlEvaluationNamedXContentProvider().getNamedXContentParsers()); } - @Override - protected AccuracyMetric.Result createTestInstance() { + public static Result randomResult() { int numClasses = randomIntBetween(2, 100); List classNames = Stream.generate(() -> randomAlphaOfLength(10)).limit(numClasses).collect(Collectors.toList()); List actualClasses = new ArrayList<>(numClasses); @@ -52,8 +51,13 @@ protected AccuracyMetric.Result createTestInstance() { } @Override - protected AccuracyMetric.Result doParseInstance(XContentParser parser) throws IOException { - return AccuracyMetric.Result.fromXContent(parser); + protected Result createTestInstance() { + return randomResult(); + } + + @Override + protected Result doParseInstance(XContentParser parser) throws IOException { + return Result.fromXContent(parser); } @Override diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/ClassificationTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/ClassificationTests.java index 491c74fc2e0ea..acb6f21cb8209 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/ClassificationTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/ClassificationTests.java @@ -38,7 +38,10 @@ protected NamedXContentRegistry xContentRegistry() { static Classification createRandom() { List metrics = - randomSubsetOf(Arrays.asList(AccuracyMetricTests.createRandom(), MulticlassConfusionMatrixMetricTests.createRandom())); + randomSubsetOf( + Arrays.asList( + AccuracyMetricTests.createRandom(), + MulticlassConfusionMatrixMetricTests.createRandom())); return new Classification(randomAlphaOfLength(10), randomAlphaOfLength(10), metrics.isEmpty() ? null : metrics); } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixMetricResultTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixMetricResultTests.java index 55b74eb94ea21..b08b10f320301 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixMetricResultTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixMetricResultTests.java @@ -40,8 +40,7 @@ protected NamedXContentRegistry xContentRegistry() { return new NamedXContentRegistry(new MlEvaluationNamedXContentProvider().getNamedXContentParsers()); } - @Override - protected Result createTestInstance() { + public static Result randomResult() { int numClasses = randomIntBetween(2, 100); List classNames = Stream.generate(() -> randomAlphaOfLength(10)).limit(numClasses).collect(Collectors.toList()); List actualClasses = new ArrayList<>(numClasses); @@ -60,6 +59,11 @@ protected Result createTestInstance() { return new Result(actualClasses, randomBoolean() ? randomNonNegativeLong() : null); } + @Override + protected Result createTestInstance() { + return randomResult(); + } + @Override protected Result doParseInstance(XContentParser parser) throws IOException { return Result.fromXContent(parser); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/AucRocMetricAucRocPointTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/AucRocMetricAucRocPointTests.java similarity index 92% rename from client/rest-high-level/src/test/java/org/elasticsearch/client/ml/AucRocMetricAucRocPointTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/AucRocMetricAucRocPointTests.java index 825adcd2060f8..93f2b25a7346f 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/AucRocMetricAucRocPointTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/AucRocMetricAucRocPointTests.java @@ -16,9 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.client.ml; +package org.elasticsearch.client.ml.dataframe.evaluation.softclassification; -import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.AucRocMetric; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/AucRocMetricResultTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/AucRocMetricResultTests.java similarity index 88% rename from client/rest-high-level/src/test/java/org/elasticsearch/client/ml/AucRocMetricResultTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/AucRocMetricResultTests.java index 9ea7689d60f32..bd8fc8e790e81 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/AucRocMetricResultTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/AucRocMetricResultTests.java @@ -16,9 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.client.ml; +package org.elasticsearch.client.ml.dataframe.evaluation.softclassification; -import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.AucRocMetric; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; @@ -27,11 +26,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.elasticsearch.client.ml.AucRocMetricAucRocPointTests.randomPoint; +import static org.elasticsearch.client.ml.dataframe.evaluation.softclassification.AucRocMetricAucRocPointTests.randomPoint; public class AucRocMetricResultTests extends AbstractXContentTestCase { - static AucRocMetric.Result randomResult() { + public static AucRocMetric.Result randomResult() { return new AucRocMetric.Result( randomDouble(), Stream diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/ConfusionMatrixMetricConfusionMatrixTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/ConfusionMatrixMetricConfusionMatrixTests.java similarity index 92% rename from client/rest-high-level/src/test/java/org/elasticsearch/client/ml/ConfusionMatrixMetricConfusionMatrixTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/ConfusionMatrixMetricConfusionMatrixTests.java index b54bcd53fc4a1..39897112f38d8 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/ConfusionMatrixMetricConfusionMatrixTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/ConfusionMatrixMetricConfusionMatrixTests.java @@ -16,9 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.client.ml; +package org.elasticsearch.client.ml.dataframe.evaluation.softclassification; -import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.ConfusionMatrixMetric; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/ConfusionMatrixMetricResultTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/ConfusionMatrixMetricResultTests.java similarity index 87% rename from client/rest-high-level/src/test/java/org/elasticsearch/client/ml/ConfusionMatrixMetricResultTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/ConfusionMatrixMetricResultTests.java index c4b299a96b536..42819e077d8cc 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/ConfusionMatrixMetricResultTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/ConfusionMatrixMetricResultTests.java @@ -16,9 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.client.ml; +package org.elasticsearch.client.ml.dataframe.evaluation.softclassification; -import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.ConfusionMatrixMetric; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; @@ -27,11 +26,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.elasticsearch.client.ml.ConfusionMatrixMetricConfusionMatrixTests.randomConfusionMatrix; +import static org.elasticsearch.client.ml.dataframe.evaluation.softclassification.ConfusionMatrixMetricConfusionMatrixTests.randomConfusionMatrix; public class ConfusionMatrixMetricResultTests extends AbstractXContentTestCase { - static ConfusionMatrixMetric.Result randomResult() { + public static ConfusionMatrixMetric.Result randomResult() { return new ConfusionMatrixMetric.Result( Stream .generate(() -> randomConfusionMatrix()) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PrecisionMetricResultTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/PrecisionMetricResultTests.java similarity index 91% rename from client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PrecisionMetricResultTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/PrecisionMetricResultTests.java index 607adacebb827..7ece003ef22e0 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PrecisionMetricResultTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/PrecisionMetricResultTests.java @@ -16,9 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.client.ml; +package org.elasticsearch.client.ml.dataframe.evaluation.softclassification; -import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.PrecisionMetric; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; @@ -29,7 +28,7 @@ public class PrecisionMetricResultTests extends AbstractXContentTestCase { - static PrecisionMetric.Result randomResult() { + public static PrecisionMetric.Result randomResult() { return new PrecisionMetric.Result( Stream .generate(() -> randomDouble()) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/RecallMetricResultTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/RecallMetricResultTests.java similarity index 91% rename from client/rest-high-level/src/test/java/org/elasticsearch/client/ml/RecallMetricResultTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/RecallMetricResultTests.java index 138875007e30d..85d9b38075e21 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/RecallMetricResultTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/RecallMetricResultTests.java @@ -16,9 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.client.ml; +package org.elasticsearch.client.ml.dataframe.evaluation.softclassification; -import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.RecallMetric; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; @@ -29,7 +28,7 @@ public class RecallMetricResultTests extends AbstractXContentTestCase { - static RecallMetric.Result randomResult() { + public static RecallMetric.Result randomResult() { return new RecallMetric.Result( Stream .generate(() -> randomDouble()) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MockAggregations.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MockAggregations.java new file mode 100644 index 0000000000000..d5919930cb818 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MockAggregations.java @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.dataframe.evaluation; + +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.bucket.filter.Filter; +import org.elasticsearch.search.aggregations.bucket.filter.Filters; +import org.elasticsearch.search.aggregations.bucket.terms.Terms; +import org.elasticsearch.search.aggregations.metrics.Cardinality; +import org.elasticsearch.search.aggregations.metrics.ExtendedStats; +import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public final class MockAggregations { + + public static Terms mockTerms(String name) { + return mockTerms(name, Collections.emptyList(), 0); + } + + public static Terms mockTerms(String name, List buckets, long sumOfOtherDocCounts) { + Terms agg = mock(Terms.class); + when(agg.getName()).thenReturn(name); + doReturn(buckets).when(agg).getBuckets(); + when(agg.getSumOfOtherDocCounts()).thenReturn(sumOfOtherDocCounts); + return agg; + } + + public static Terms.Bucket mockTermsBucket(String key, Aggregations subAggs) { + Terms.Bucket bucket = mock(Terms.Bucket.class); + when(bucket.getKeyAsString()).thenReturn(key); + when(bucket.getAggregations()).thenReturn(subAggs); + return bucket; + } + + public static Filters mockFilters(String name) { + return mockFilters(name, Collections.emptyList()); + } + + public static Filters mockFilters(String name, List buckets) { + Filters agg = mock(Filters.class); + when(agg.getName()).thenReturn(name); + doReturn(buckets).when(agg).getBuckets(); + return agg; + } + + public static Filters.Bucket mockFiltersBucket(String key, long docCount, Aggregations subAggs) { + Filters.Bucket bucket = mockFiltersBucket(key, docCount); + when(bucket.getAggregations()).thenReturn(subAggs); + return bucket; + } + + public static Filters.Bucket mockFiltersBucket(String key, long docCount) { + Filters.Bucket bucket = mock(Filters.Bucket.class); + when(bucket.getKeyAsString()).thenReturn(key); + when(bucket.getDocCount()).thenReturn(docCount); + return bucket; + } + + public static Filter mockFilter(String name, long docCount) { + Filter agg = mock(Filter.class); + when(agg.getName()).thenReturn(name); + when(agg.getDocCount()).thenReturn(docCount); + return agg; + } + + public static NumericMetricsAggregation.SingleValue mockSingleValue(String name, double value) { + NumericMetricsAggregation.SingleValue agg = mock(NumericMetricsAggregation.SingleValue.class); + when(agg.getName()).thenReturn(name); + when(agg.value()).thenReturn(value); + return agg; + } + + public static Cardinality mockCardinality(String name, long value) { + Cardinality agg = mock(Cardinality.class); + when(agg.getName()).thenReturn(name); + when(agg.getValue()).thenReturn(value); + return agg; + } + + public static ExtendedStats mockExtendedStats(String name, double variance, long count) { + ExtendedStats agg = mock(ExtendedStats.class); + when(agg.getName()).thenReturn(name); + when(agg.getVariance()).thenReturn(variance); + when(agg.getCount()).thenReturn(count); + return agg; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java index 4a784400c3b65..1809f0e735125 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java @@ -8,17 +8,15 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.Aggregations; -import org.elasticsearch.search.aggregations.bucket.terms.Terms; -import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; import org.elasticsearch.test.AbstractSerializingTestCase; import java.io.IOException; import java.util.Arrays; import java.util.List; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockSingleValue; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockTerms; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class AccuracyTests extends AbstractSerializingTestCase { @@ -48,9 +46,9 @@ public static Accuracy createRandom() { public void testProcess() { Aggregations aggs = new Aggregations(Arrays.asList( - createTermsAgg("classification_classes"), - createSingleMetricAgg("classification_overall_accuracy", 0.8123), - createSingleMetricAgg("some_other_single_metric_agg", 0.2377) + mockTerms("classification_classes"), + mockSingleValue("classification_overall_accuracy", 0.8123), + mockSingleValue("some_other_single_metric_agg", 0.2377) )); Accuracy accuracy = new Accuracy(); @@ -62,16 +60,16 @@ public void testProcess() { public void testProcess_GivenMissingAgg() { { Aggregations aggs = new Aggregations(Arrays.asList( - createTermsAgg("classification_classes"), - createSingleMetricAgg("some_other_single_metric_agg", 0.2377) + mockTerms("classification_classes"), + mockSingleValue("some_other_single_metric_agg", 0.2377) )); Accuracy accuracy = new Accuracy(); expectThrows(NullPointerException.class, () -> accuracy.process(aggs)); } { Aggregations aggs = new Aggregations(Arrays.asList( - createSingleMetricAgg("classification_overall_accuracy", 0.8123), - createSingleMetricAgg("some_other_single_metric_agg", 0.2377) + mockSingleValue("classification_overall_accuracy", 0.8123), + mockSingleValue("some_other_single_metric_agg", 0.2377) )); Accuracy accuracy = new Accuracy(); expectThrows(NullPointerException.class, () -> accuracy.process(aggs)); @@ -81,32 +79,19 @@ public void testProcess_GivenMissingAgg() { public void testProcess_GivenAggOfWrongType() { { Aggregations aggs = new Aggregations(Arrays.asList( - createTermsAgg("classification_classes"), - createTermsAgg("classification_overall_accuracy") + mockTerms("classification_classes"), + mockTerms("classification_overall_accuracy") )); Accuracy accuracy = new Accuracy(); expectThrows(ClassCastException.class, () -> accuracy.process(aggs)); } { Aggregations aggs = new Aggregations(Arrays.asList( - createSingleMetricAgg("classification_classes", 1.0), - createSingleMetricAgg("classification_overall_accuracy", 0.8123) + mockSingleValue("classification_classes", 1.0), + mockSingleValue("classification_overall_accuracy", 0.8123) )); Accuracy accuracy = new Accuracy(); expectThrows(ClassCastException.class, () -> accuracy.process(aggs)); } } - - private static NumericMetricsAggregation.SingleValue createSingleMetricAgg(String name, double value) { - NumericMetricsAggregation.SingleValue agg = mock(NumericMetricsAggregation.SingleValue.class); - when(agg.getName()).thenReturn(name); - when(agg.value()).thenReturn(value); - return agg; - } - - private static Terms createTermsAgg(String name) { - Terms agg = mock(Terms.class); - when(agg.getName()).thenReturn(name); - return agg; - } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java index 5dfe61768f7d9..23c2effb37fe9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java @@ -52,7 +52,10 @@ protected NamedXContentRegistry xContentRegistry() { public static Classification createRandom() { List metrics = - randomSubsetOf(Arrays.asList(AccuracyTests.createRandom(), MulticlassConfusionMatrixTests.createRandom())); + randomSubsetOf( + Arrays.asList( + AccuracyTests.createRandom(), + MulticlassConfusionMatrixTests.createRandom())); return new Classification(randomAlphaOfLength(10), randomAlphaOfLength(10), metrics.isEmpty() ? null : metrics); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java index 0b4f724549e1a..a04e47ff26e65 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java @@ -10,24 +10,23 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.Aggregations; -import org.elasticsearch.search.aggregations.bucket.filter.Filters; -import org.elasticsearch.search.aggregations.bucket.terms.Terms; -import org.elasticsearch.search.aggregations.metrics.Cardinality; import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.ActualClass; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.PredictedClass; import java.io.IOException; import java.util.List; -import java.util.Optional; +import static org.elasticsearch.test.hamcrest.OptionalMatchers.isEmpty; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockCardinality; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockFilters; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockFiltersBucket; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockTerms; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockTermsBucket; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class MulticlassConfusionMatrixTests extends AbstractSerializingTestCase { @@ -75,7 +74,7 @@ public void testAggs() { MulticlassConfusionMatrix confusionMatrix = new MulticlassConfusionMatrix(); List aggs = confusionMatrix.aggs("act", "pred"); assertThat(aggs, is(not(empty()))); - assertThat(confusionMatrix.getResult(), equalTo(Optional.empty())); + assertThat(confusionMatrix.getResult(), isEmpty()); } public void testEvaluate() { @@ -157,46 +156,4 @@ public void testEvaluate_OtherClassesCountGreaterThanZero() { new ActualClass("cat", 85, List.of(new PredictedClass("cat", 30L), new PredictedClass("dog", 40L)), 15)))); assertThat(result.getOtherActualClassCount(), equalTo(3L)); } - - private static Terms mockTerms(String name, List buckets, long sumOfOtherDocCounts) { - Terms aggregation = mock(Terms.class); - when(aggregation.getName()).thenReturn(name); - doReturn(buckets).when(aggregation).getBuckets(); - when(aggregation.getSumOfOtherDocCounts()).thenReturn(sumOfOtherDocCounts); - return aggregation; - } - - private static Terms.Bucket mockTermsBucket(String key, Aggregations subAggs) { - Terms.Bucket bucket = mock(Terms.Bucket.class); - when(bucket.getKeyAsString()).thenReturn(key); - when(bucket.getAggregations()).thenReturn(subAggs); - return bucket; - } - - private static Filters mockFilters(String name, List buckets) { - Filters aggregation = mock(Filters.class); - when(aggregation.getName()).thenReturn(name); - doReturn(buckets).when(aggregation).getBuckets(); - return aggregation; - } - - private static Filters.Bucket mockFiltersBucket(String key, long docCount, Aggregations subAggs) { - Filters.Bucket bucket = mockFiltersBucket(key, docCount); - when(bucket.getAggregations()).thenReturn(subAggs); - return bucket; - } - - private static Filters.Bucket mockFiltersBucket(String key, long docCount) { - Filters.Bucket bucket = mock(Filters.Bucket.class); - when(bucket.getKeyAsString()).thenReturn(key); - when(bucket.getDocCount()).thenReturn(docCount); - return bucket; - } - - private static Cardinality mockCardinality(String name, long value) { - Cardinality aggregation = mock(Cardinality.class); - when(aggregation.getName()).thenReturn(name); - when(aggregation.getValue()).thenReturn(value); - return aggregation; - } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredErrorTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredErrorTests.java index 2516b2fea94a5..5679655c16555 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredErrorTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredErrorTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.Aggregations; -import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -17,9 +16,8 @@ import java.util.Arrays; import java.util.Collections; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockSingleValue; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class MeanSquaredErrorTests extends AbstractSerializingTestCase { @@ -44,8 +42,8 @@ public static MeanSquaredError createRandom() { public void testEvaluate() { Aggregations aggs = new Aggregations(Arrays.asList( - createSingleMetricAgg("regression_mean_squared_error", 0.8123), - createSingleMetricAgg("some_other_single_metric_agg", 0.2377) + mockSingleValue("regression_mean_squared_error", 0.8123), + mockSingleValue("some_other_single_metric_agg", 0.2377) )); MeanSquaredError mse = new MeanSquaredError(); @@ -58,7 +56,7 @@ public void testEvaluate() { public void testEvaluate_GivenMissingAggs() { Aggregations aggs = new Aggregations(Collections.singletonList( - createSingleMetricAgg("some_other_single_metric_agg", 0.2377) + mockSingleValue("some_other_single_metric_agg", 0.2377) )); MeanSquaredError mse = new MeanSquaredError(); @@ -67,11 +65,4 @@ public void testEvaluate_GivenMissingAggs() { EvaluationMetricResult result = mse.getResult().get(); assertThat(result, equalTo(new MeanSquaredError.Result(0.0))); } - - private static NumericMetricsAggregation.SingleValue createSingleMetricAgg(String name, double value) { - NumericMetricsAggregation.SingleValue agg = mock(NumericMetricsAggregation.SingleValue.class); - when(agg.getName()).thenReturn(name); - when(agg.value()).thenReturn(value); - return agg; - } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquaredTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquaredTests.java index 8c637c9cf179a..bd31ae08e2d2d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquaredTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquaredTests.java @@ -9,8 +9,6 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.Aggregations; -import org.elasticsearch.search.aggregations.metrics.ExtendedStats; -import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -18,9 +16,9 @@ import java.util.Arrays; import java.util.Collections; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockExtendedStats; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockSingleValue; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class RSquaredTests extends AbstractSerializingTestCase { @@ -45,10 +43,10 @@ public static RSquared createRandom() { public void testEvaluate() { Aggregations aggs = new Aggregations(Arrays.asList( - createSingleMetricAgg("residual_sum_of_squares", 10_111), - createExtendedStatsAgg("extended_stats_actual", 155.23, 1000), - createExtendedStatsAgg("some_other_extended_stats",99.1, 10_000), - createSingleMetricAgg("some_other_single_metric_agg", 0.2377) + mockSingleValue("residual_sum_of_squares", 10_111), + mockExtendedStats("extended_stats_actual", 155.23, 1000), + mockExtendedStats("some_other_extended_stats",99.1, 10_000), + mockSingleValue("some_other_single_metric_agg", 0.2377) )); RSquared rSquared = new RSquared(); @@ -61,10 +59,10 @@ public void testEvaluate() { public void testEvaluateWithZeroCount() { Aggregations aggs = new Aggregations(Arrays.asList( - createSingleMetricAgg("residual_sum_of_squares", 0), - createExtendedStatsAgg("extended_stats_actual", 0.0, 0), - createExtendedStatsAgg("some_other_extended_stats",99.1, 10_000), - createSingleMetricAgg("some_other_single_metric_agg", 0.2377) + mockSingleValue("residual_sum_of_squares", 0), + mockExtendedStats("extended_stats_actual", 0.0, 0), + mockExtendedStats("some_other_extended_stats",99.1, 10_000), + mockSingleValue("some_other_single_metric_agg", 0.2377) )); RSquared rSquared = new RSquared(); @@ -76,10 +74,10 @@ public void testEvaluateWithZeroCount() { public void testEvaluateWithSingleCountZeroVariance() { Aggregations aggs = new Aggregations(Arrays.asList( - createSingleMetricAgg("residual_sum_of_squares", 1), - createExtendedStatsAgg("extended_stats_actual", 0.0, 1), - createExtendedStatsAgg("some_other_extended_stats",99.1, 10_000), - createSingleMetricAgg("some_other_single_metric_agg", 0.2377) + mockSingleValue("residual_sum_of_squares", 1), + mockExtendedStats("extended_stats_actual", 0.0, 1), + mockExtendedStats("some_other_extended_stats",99.1, 10_000), + mockSingleValue("some_other_single_metric_agg", 0.2377) )); RSquared rSquared = new RSquared(); @@ -91,7 +89,7 @@ public void testEvaluateWithSingleCountZeroVariance() { public void testEvaluate_GivenMissingAggs() { Aggregations aggs = new Aggregations(Collections.singletonList( - createSingleMetricAgg("some_other_single_metric_agg", 0.2377) + mockSingleValue("some_other_single_metric_agg", 0.2377) )); RSquared rSquared = new RSquared(); @@ -103,8 +101,8 @@ public void testEvaluate_GivenMissingAggs() { public void testEvaluate_GivenMissingExtendedStatsAgg() { Aggregations aggs = new Aggregations(Arrays.asList( - createSingleMetricAgg("some_other_single_metric_agg", 0.2377), - createSingleMetricAgg("residual_sum_of_squares", 0.2377) + mockSingleValue("some_other_single_metric_agg", 0.2377), + mockSingleValue("residual_sum_of_squares", 0.2377) )); RSquared rSquared = new RSquared(); @@ -116,8 +114,8 @@ public void testEvaluate_GivenMissingExtendedStatsAgg() { public void testEvaluate_GivenMissingResidualSumOfSquaresAgg() { Aggregations aggs = new Aggregations(Arrays.asList( - createSingleMetricAgg("some_other_single_metric_agg", 0.2377), - createExtendedStatsAgg("extended_stats_actual",100, 50) + mockSingleValue("some_other_single_metric_agg", 0.2377), + mockExtendedStats("extended_stats_actual",100, 50) )); RSquared rSquared = new RSquared(); @@ -126,19 +124,4 @@ public void testEvaluate_GivenMissingResidualSumOfSquaresAgg() { EvaluationMetricResult result = rSquared.getResult().get(); assertThat(result, equalTo(new RSquared.Result(0.0))); } - - private static NumericMetricsAggregation.SingleValue createSingleMetricAgg(String name, double value) { - NumericMetricsAggregation.SingleValue agg = mock(NumericMetricsAggregation.SingleValue.class); - when(agg.getName()).thenReturn(name); - when(agg.value()).thenReturn(value); - return agg; - } - - private static ExtendedStats createExtendedStatsAgg(String name, double variance, long count) { - ExtendedStats agg = mock(ExtendedStats.class); - when(agg.getName()).thenReturn(name); - when(agg.getVariance()).thenReturn(variance); - when(agg.getCount()).thenReturn(count); - return agg; - } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ConfusionMatrixTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ConfusionMatrixTests.java index cf54131af137e..84194bd0bac09 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ConfusionMatrixTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ConfusionMatrixTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.Aggregations; -import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -18,9 +17,8 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockFilter; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class ConfusionMatrixTests extends AbstractSerializingTestCase { @@ -50,14 +48,14 @@ public static ConfusionMatrix createRandom() { public void testEvaluate() { Aggregations aggs = new Aggregations(Arrays.asList( - createFilterAgg("confusion_matrix_at_0.25_TP", 1L), - createFilterAgg("confusion_matrix_at_0.25_FP", 2L), - createFilterAgg("confusion_matrix_at_0.25_TN", 3L), - createFilterAgg("confusion_matrix_at_0.25_FN", 4L), - createFilterAgg("confusion_matrix_at_0.5_TP", 5L), - createFilterAgg("confusion_matrix_at_0.5_FP", 6L), - createFilterAgg("confusion_matrix_at_0.5_TN", 7L), - createFilterAgg("confusion_matrix_at_0.5_FN", 8L) + mockFilter("confusion_matrix_at_0.25_TP", 1L), + mockFilter("confusion_matrix_at_0.25_FP", 2L), + mockFilter("confusion_matrix_at_0.25_TN", 3L), + mockFilter("confusion_matrix_at_0.25_FN", 4L), + mockFilter("confusion_matrix_at_0.5_TP", 5L), + mockFilter("confusion_matrix_at_0.5_FP", 6L), + mockFilter("confusion_matrix_at_0.5_TN", 7L), + mockFilter("confusion_matrix_at_0.5_FN", 8L) )); ConfusionMatrix confusionMatrix = new ConfusionMatrix(Arrays.asList(0.25, 0.5)); @@ -66,11 +64,4 @@ public void testEvaluate() { String expected = "{\"0.25\":{\"tp\":1,\"fp\":2,\"tn\":3,\"fn\":4},\"0.5\":{\"tp\":5,\"fp\":6,\"tn\":7,\"fn\":8}}"; assertThat(Strings.toString(result), equalTo(expected)); } - - private static Filter createFilterAgg(String name, long docCount) { - Filter agg = mock(Filter.class); - when(agg.getName()).thenReturn(name); - when(agg.getDocCount()).thenReturn(docCount); - return agg; - } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/PrecisionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/PrecisionTests.java index 58f2864fd0747..faf7c9ac0e7f2 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/PrecisionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/PrecisionTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.Aggregations; -import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -18,9 +17,8 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockFilter; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class PrecisionTests extends AbstractSerializingTestCase { @@ -50,12 +48,12 @@ public static Precision createRandom() { public void testEvaluate() { Aggregations aggs = new Aggregations(Arrays.asList( - createFilterAgg("precision_at_0.25_TP", 1L), - createFilterAgg("precision_at_0.25_FP", 4L), - createFilterAgg("precision_at_0.5_TP", 3L), - createFilterAgg("precision_at_0.5_FP", 1L), - createFilterAgg("precision_at_0.75_TP", 5L), - createFilterAgg("precision_at_0.75_FP", 0L) + mockFilter("precision_at_0.25_TP", 1L), + mockFilter("precision_at_0.25_FP", 4L), + mockFilter("precision_at_0.5_TP", 3L), + mockFilter("precision_at_0.5_FP", 1L), + mockFilter("precision_at_0.75_TP", 5L), + mockFilter("precision_at_0.75_FP", 0L) )); Precision precision = new Precision(Arrays.asList(0.25, 0.5, 0.75)); @@ -67,8 +65,8 @@ public void testEvaluate() { public void testEvaluate_GivenZeroTpAndFp() { Aggregations aggs = new Aggregations(Arrays.asList( - createFilterAgg("precision_at_1.0_TP", 0L), - createFilterAgg("precision_at_1.0_FP", 0L) + mockFilter("precision_at_1.0_TP", 0L), + mockFilter("precision_at_1.0_FP", 0L) )); Precision precision = new Precision(Arrays.asList(1.0)); @@ -77,11 +75,4 @@ public void testEvaluate_GivenZeroTpAndFp() { String expected = "{\"1.0\":0.0}"; assertThat(Strings.toString(result), equalTo(expected)); } - - private static Filter createFilterAgg(String name, long docCount) { - Filter agg = mock(Filter.class); - when(agg.getName()).thenReturn(name); - when(agg.getDocCount()).thenReturn(docCount); - return agg; - } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/RecallTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/RecallTests.java index 009805425cd88..343d19059551c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/RecallTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/RecallTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.Aggregations; -import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -18,9 +17,8 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockFilter; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class RecallTests extends AbstractSerializingTestCase { @@ -50,12 +48,12 @@ public static Recall createRandom() { public void testEvaluate() { Aggregations aggs = new Aggregations(Arrays.asList( - createFilterAgg("recall_at_0.25_TP", 1L), - createFilterAgg("recall_at_0.25_FN", 4L), - createFilterAgg("recall_at_0.5_TP", 3L), - createFilterAgg("recall_at_0.5_FN", 1L), - createFilterAgg("recall_at_0.75_TP", 5L), - createFilterAgg("recall_at_0.75_FN", 0L) + mockFilter("recall_at_0.25_TP", 1L), + mockFilter("recall_at_0.25_FN", 4L), + mockFilter("recall_at_0.5_TP", 3L), + mockFilter("recall_at_0.5_FN", 1L), + mockFilter("recall_at_0.75_TP", 5L), + mockFilter("recall_at_0.75_FN", 0L) )); Recall recall = new Recall(Arrays.asList(0.25, 0.5, 0.75)); @@ -67,8 +65,8 @@ public void testEvaluate() { public void testEvaluate_GivenZeroTpAndFp() { Aggregations aggs = new Aggregations(Arrays.asList( - createFilterAgg("recall_at_1.0_TP", 0L), - createFilterAgg("recall_at_1.0_FN", 0L) + mockFilter("recall_at_1.0_TP", 0L), + mockFilter("recall_at_1.0_FN", 0L) )); Recall recall = new Recall(Arrays.asList(1.0)); @@ -77,11 +75,4 @@ public void testEvaluate_GivenZeroTpAndFp() { String expected = "{\"1.0\":0.0}"; assertThat(Strings.toString(result), equalTo(expected)); } - - private static Filter createFilterAgg(String name, long docCount) { - Filter agg = mock(Filter.class); - when(agg.getName()).thenReturn(name); - when(agg.getDocCount()).thenReturn(docCount); - return agg; - } } From 3afe18226944b1a39cc6d09ffb5a51c36ef321dd Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Tue, 3 Dec 2019 09:34:03 -0700 Subject: [PATCH 054/686] Ensure remote strategy settings can be updated (#49772) This is related to #49067. As part of this work a new sniff number of node connections setting, a simple addresses setting, and a simple number of sockets setting have been added. This commit ensures that these settings are properly hooked up to support dynamic updates. --- .../transport/SimpleConnectionStrategy.java | 17 +++++- .../transport/SniffConnectionStrategy.java | 4 +- .../SimpleConnectionStrategyTests.java | 54 +++++++++++++++++++ .../SniffConnectionStrategyTests.java | 10 +++- 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java index 839a1d19285b7..890cdaf25387b 100644 --- a/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java @@ -32,7 +32,9 @@ import org.elasticsearch.common.util.concurrent.CountDown; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -74,6 +76,7 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { private final int maxNumConnections; private final AtomicLong counter = new AtomicLong(0); + private final List configuredAddresses; private final List> addresses; private final AtomicReference remoteClusterName = new AtomicReference<>(); private final ConnectionProfile profile; @@ -100,6 +103,7 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { int maxNumConnections, List configuredAddresses, List> addresses) { super(clusterAlias, transportService, connectionManager); this.maxNumConnections = maxNumConnections; + this.configuredAddresses = configuredAddresses; assert addresses.isEmpty() == false : "Cannot use simple connection strategy with no configured addresses"; this.addresses = addresses; // TODO: Move into the ConnectionManager @@ -134,7 +138,9 @@ protected boolean shouldOpenMoreConnections() { @Override protected boolean strategyMustBeRebuilt(Settings newSettings) { - return false; + List addresses = REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias).get(newSettings); + int numOfSockets = REMOTE_SOCKET_CONNECTIONS.getConcreteSettingForNamespace(clusterAlias).get(newSettings); + return numOfSockets != maxNumConnections || addressesChanged(configuredAddresses, addresses); } @Override @@ -223,4 +229,13 @@ private TransportAddress nextAddress(List resolvedAddresses) { private static TransportAddress resolveAddress(String address) { return new TransportAddress(parseSeedAddress(address)); } + + private boolean addressesChanged(final List oldAddresses, final List newAddresses) { + if (oldAddresses.size() != newAddresses.size()) { + return true; + } + Set oldSeeds = new HashSet<>(oldAddresses); + Set newSeeds = new HashSet<>(newAddresses); + return oldSeeds.equals(newSeeds) == false; + } } diff --git a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java index ee56629ebf0aa..9ec0f4afe9997 100644 --- a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java @@ -202,7 +202,9 @@ protected boolean shouldOpenMoreConnections() { protected boolean strategyMustBeRebuilt(Settings newSettings) { String proxy = REMOTE_CLUSTERS_PROXY.getConcreteSettingForNamespace(clusterAlias).get(newSettings); List addresses = REMOTE_CLUSTER_SEEDS.getConcreteSettingForNamespace(clusterAlias).get(newSettings); - return seedsChanged(configuredSeedNodes, addresses) || proxyChanged(proxyAddress, proxy); + int nodeConnections = REMOTE_NODE_CONNECTIONS.getConcreteSettingForNamespace(clusterAlias).get(newSettings); + return nodeConnections != maxNumRemoteConnections || seedsChanged(configuredSeedNodes, addresses) || + proxyChanged(proxyAddress, proxy); } @Override diff --git a/server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java b/server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java index 35a6b7a6758ac..4144cc856bd3a 100644 --- a/server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java +++ b/server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.Version; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.AbstractScopedSettings; import org.elasticsearch.common.settings.ClusterSettings; @@ -303,6 +304,59 @@ numOfConnections, addresses(address), Collections.singletonList(addressSupplier } } + public void testSimpleStrategyWillNeedToBeRebuiltIfNumOfSocketsOrAddressesChange() { + try (MockTransportService transport1 = startTransport("node1", Version.CURRENT); + MockTransportService transport2 = startTransport("node2", Version.CURRENT)) { + TransportAddress address1 = transport1.boundAddress().publishAddress(); + TransportAddress address2 = transport2.boundAddress().publishAddress(); + + try (MockTransportService localService = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool)) { + localService.start(); + localService.acceptIncomingRequests(); + + ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); + int numOfConnections = randomIntBetween(4, 8); + try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); + SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, + numOfConnections, addresses(address1, address2))) { + PlainActionFuture connectFuture = PlainActionFuture.newFuture(); + strategy.connect(connectFuture); + connectFuture.actionGet(); + + assertTrue(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); + assertTrue(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); + assertEquals(numOfConnections, connectionManager.size()); + assertTrue(strategy.assertNoRunningConnections()); + + Setting modeSetting = RemoteConnectionStrategy.REMOTE_CONNECTION_MODE + .getConcreteSettingForNamespace("cluster-alias"); + Setting addressesSetting = SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES + .getConcreteSettingForNamespace("cluster-alias"); + Setting socketConnections = SimpleConnectionStrategy.REMOTE_SOCKET_CONNECTIONS + .getConcreteSettingForNamespace("cluster-alias"); + + Settings noChange = Settings.builder() + .put(modeSetting.getKey(), "simple") + .put(addressesSetting.getKey(), Strings.arrayToCommaDelimitedString(addresses(address1, address2).toArray())) + .put(socketConnections.getKey(), numOfConnections) + .build(); + assertFalse(strategy.shouldRebuildConnection(noChange)); + Settings addressesChanged = Settings.builder() + .put(modeSetting.getKey(), "simple") + .put(addressesSetting.getKey(), Strings.arrayToCommaDelimitedString(addresses(address1).toArray())) + .build(); + assertTrue(strategy.shouldRebuildConnection(addressesChanged)); + Settings socketsChanged = Settings.builder() + .put(modeSetting.getKey(), "simple") + .put(addressesSetting.getKey(), Strings.arrayToCommaDelimitedString(addresses(address1, address2).toArray())) + .put(socketConnections.getKey(), numOfConnections + 1) + .build(); + assertTrue(strategy.shouldRebuildConnection(socketsChanged)); + } + } + } + } + public void testModeSettingsCannotBeUsedWhenInDifferentMode() { List, String>> restrictedSettings = Arrays.asList( new Tuple<>(SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES, "192.168.0.1:8080"), diff --git a/server/src/test/java/org/elasticsearch/transport/SniffConnectionStrategyTests.java b/server/src/test/java/org/elasticsearch/transport/SniffConnectionStrategyTests.java index 721055a9c20f7..30f30723c19a8 100644 --- a/server/src/test/java/org/elasticsearch/transport/SniffConnectionStrategyTests.java +++ b/server/src/test/java/org/elasticsearch/transport/SniffConnectionStrategyTests.java @@ -487,7 +487,7 @@ public void testConfiguredProxyAddressModeWillReplaceNodeAddress() { } } - public void testSniffStrategyWillNeedToBeRebuiltIfSeedsOrProxyChange() { + public void testSniffStrategyWillNeedToBeRebuiltIfNumOfConnectionsOrSeedsOrProxyChange() { List knownNodes = new CopyOnWriteArrayList<>(); try (MockTransportService seedTransport = startTransport("seed_node", knownNodes, Version.CURRENT); MockTransportService discoverableTransport = startTransport("discoverable_node", knownNodes, Version.CURRENT)) { @@ -516,9 +516,12 @@ public void testSniffStrategyWillNeedToBeRebuiltIfSeedsOrProxyChange() { Setting seedSetting = SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS.getConcreteSettingForNamespace("cluster-alias"); Setting proxySetting = SniffConnectionStrategy.REMOTE_CLUSTERS_PROXY.getConcreteSettingForNamespace("cluster-alias"); + Setting numConnections = SniffConnectionStrategy.REMOTE_NODE_CONNECTIONS + .getConcreteSettingForNamespace("cluster-alias"); Settings noChange = Settings.builder() .put(seedSetting.getKey(), Strings.arrayToCommaDelimitedString(seedNodes(seedNode).toArray())) + .put(numConnections.getKey(), 3) .build(); assertFalse(strategy.shouldRebuildConnection(noChange)); Settings seedsChanged = Settings.builder() @@ -530,6 +533,11 @@ public void testSniffStrategyWillNeedToBeRebuiltIfSeedsOrProxyChange() { .put(proxySetting.getKey(), "proxy_address:9300") .build(); assertTrue(strategy.shouldRebuildConnection(proxyChanged)); + Settings connectionsChanged = Settings.builder() + .put(seedSetting.getKey(), Strings.arrayToCommaDelimitedString(seedNodes(seedNode).toArray())) + .put(numConnections.getKey(), 4) + .build(); + assertTrue(strategy.shouldRebuildConnection(connectionsChanged)); } } } From a0bc15245388e8d9ebb5fbaba87b0182141f32c4 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 3 Dec 2019 14:20:32 -0500 Subject: [PATCH 055/686] Extend systemd timeout during startup (#49784) When we are notifying systemd that we are fully started up, it can be that we do not notify systemd before its default timeout of sixty seconds elapses (e.g., if we are upgrading on-disk metadata). In this case, we need to notify systemd to extend this timeout so that we are not abruptly terminated. We do this by repeatedly sending EXTEND_TIMEOUT_USEC to extend the timeout by thirty seconds; we do this every fifteen seconds. This will prevent systemd from abruptly terminating us during a long startup. We cancel the scheduled execution of this notification after we have successfully started up. --- .../elasticsearch/systemd/SystemdPlugin.java | 57 +++++++++++++++++- .../systemd/SystemdPluginTests.java | 58 +++++++++++++++---- 2 files changed, 100 insertions(+), 15 deletions(-) diff --git a/modules/systemd/src/main/java/org/elasticsearch/systemd/SystemdPlugin.java b/modules/systemd/src/main/java/org/elasticsearch/systemd/SystemdPlugin.java index 56a5cee808f23..cdc6ba2e31b06 100644 --- a/modules/systemd/src/main/java/org/elasticsearch/systemd/SystemdPlugin.java +++ b/modules/systemd/src/main/java/org/elasticsearch/systemd/SystemdPlugin.java @@ -22,8 +22,22 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.Build; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.plugins.ClusterPlugin; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.threadpool.Scheduler; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; + +import java.util.Collection; +import java.util.List; public class SystemdPlugin extends Plugin implements ClusterPlugin { @@ -62,8 +76,44 @@ public SystemdPlugin() { enabled = Boolean.TRUE.toString().equals(esSDNotify); } + Scheduler.Cancellable extender; + + @Override + public Collection createComponents( + final Client client, + final ClusterService clusterService, + final ThreadPool threadPool, + final ResourceWatcherService resourceWatcherService, + final ScriptService scriptService, + final NamedXContentRegistry xContentRegistry, + final Environment environment, + final NodeEnvironment nodeEnvironment, + final NamedWriteableRegistry namedWriteableRegistry) { + if (enabled) { + /* + * Since we have set the service type to notify, by default systemd will wait up to sixty seconds for the process to send the + * READY=1 status via sd_notify. Since our startup can take longer than that (e.g., if we are upgrading on-disk metadata) then + * we need to repeatedly notify systemd that we are still starting up by sending EXTEND_TIMEOUT_USEC with an extension to the + * timeout. Therefore, every fifteen seconds we send systemd a message via sd_notify to extend the timeout by thirty seconds. + * We will cancel this scheduled task after we successfully notify systemd that we are ready. + */ + extender = threadPool.scheduleWithFixedDelay( + () -> { + final int rc = sd_notify(0, "EXTEND_TIMEOUT_USEC=30000000"); + if (rc < 0) { + logger.warn("extending startup timeout via sd_notify failed with [{}]", rc); + } + }, + TimeValue.timeValueSeconds(15), + ThreadPool.Names.SAME); + } + return List.of(); + } + int sd_notify(@SuppressWarnings("SameParameterValue") final int unset_environment, final String state) { - return Libsystemd.sd_notify(unset_environment, state); + final int rc = Libsystemd.sd_notify(unset_environment, state); + logger.trace("sd_notify({}, {}) returned [{}]", unset_environment, state, rc); + return rc; } @Override @@ -72,11 +122,13 @@ public void onNodeStarted() { return; } final int rc = sd_notify(0, "READY=1"); - logger.trace("sd_notify returned [{}]", rc); if (rc < 0) { // treat failure to notify systemd of readiness as a startup failure throw new RuntimeException("sd_notify returned error [" + rc + "]"); } + assert extender != null; + final boolean cancelled = extender.cancel(); + assert cancelled; } @Override @@ -85,7 +137,6 @@ public void close() { return; } final int rc = sd_notify(0, "STOPPING=1"); - logger.trace("sd_notify returned [{}]", rc); if (rc < 0) { // do not treat failure to notify systemd of stopping as a failure logger.warn("sd_notify returned error [{}]", rc); diff --git a/modules/systemd/src/test/java/org/elasticsearch/systemd/SystemdPluginTests.java b/modules/systemd/src/test/java/org/elasticsearch/systemd/SystemdPluginTests.java index 85f1446e4b0ac..13bfe5328f75c 100644 --- a/modules/systemd/src/test/java/org/elasticsearch/systemd/SystemdPluginTests.java +++ b/modules/systemd/src/test/java/org/elasticsearch/systemd/SystemdPluginTests.java @@ -21,20 +21,28 @@ import org.elasticsearch.Build; import org.elasticsearch.common.CheckedConsumer; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.hamcrest.OptionalMatchers; +import org.elasticsearch.threadpool.Scheduler; +import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; +import java.util.function.BiConsumer; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class SystemdPluginTests extends ESTestCase { @@ -42,24 +50,41 @@ public class SystemdPluginTests extends ESTestCase { private Build.Type randomNonPackageBuildType = randomValueOtherThanMany(t -> t == Build.Type.DEB || t == Build.Type.RPM, () -> randomFrom(Build.Type.values())); + final Scheduler.Cancellable extender = mock(Scheduler.Cancellable.class); + final ThreadPool threadPool = mock(ThreadPool.class); + + { + when(extender.cancel()).thenReturn(true); + when(threadPool.scheduleWithFixedDelay(any(Runnable.class), eq(TimeValue.timeValueSeconds(15)), eq(ThreadPool.Names.SAME))) + .thenReturn(extender); + } + public void testIsEnabled() { final SystemdPlugin plugin = new SystemdPlugin(false, randomPackageBuildType, Boolean.TRUE.toString()); + plugin.createComponents(null, null, threadPool, null, null, null, null, null, null); assertTrue(plugin.isEnabled()); + assertNotNull(plugin.extender); } public void testIsNotPackageDistribution() { final SystemdPlugin plugin = new SystemdPlugin(false, randomNonPackageBuildType, Boolean.TRUE.toString()); + plugin.createComponents(null, null, threadPool, null, null, null, null, null, null); assertFalse(plugin.isEnabled()); + assertNull(plugin.extender); } public void testIsImplicitlyNotEnabled() { final SystemdPlugin plugin = new SystemdPlugin(false, randomPackageBuildType, null); + plugin.createComponents(null, null, threadPool, null, null, null, null, null, null); assertFalse(plugin.isEnabled()); + assertNull(plugin.extender); } public void testIsExplicitlyNotEnabled() { final SystemdPlugin plugin = new SystemdPlugin(false, randomPackageBuildType, Boolean.FALSE.toString()); + plugin.createComponents(null, null, threadPool, null, null, null, null, null, null); assertFalse(plugin.isEnabled()); + assertNull(plugin.extender); } public void testInvalid() { @@ -75,7 +100,10 @@ public void testOnNodeStartedSuccess() { runTestOnNodeStarted( Boolean.TRUE.toString(), randomIntBetween(0, Integer.MAX_VALUE), - maybe -> assertThat(maybe, OptionalMatchers.isEmpty())); + (maybe, plugin) -> { + assertThat(maybe, OptionalMatchers.isEmpty()); + verify(plugin.extender).cancel(); + }); } public void testOnNodeStartedFailure() { @@ -83,7 +111,7 @@ public void testOnNodeStartedFailure() { runTestOnNodeStarted( Boolean.TRUE.toString(), rc, - maybe -> { + (maybe, plugin) -> { assertThat(maybe, OptionalMatchers.isPresent()); // noinspection OptionalGetWithoutIsPresent assertThat(maybe.get(), instanceOf(RuntimeException.class)); @@ -95,13 +123,13 @@ public void testOnNodeStartedNotEnabled() { runTestOnNodeStarted( Boolean.FALSE.toString(), randomInt(), - maybe -> assertThat(maybe, OptionalMatchers.isEmpty())); + (maybe, plugin) -> assertThat(maybe, OptionalMatchers.isEmpty())); } private void runTestOnNodeStarted( final String esSDNotify, final int rc, - final Consumer> assertions) { + final BiConsumer, SystemdPlugin> assertions) { runTest(esSDNotify, rc, assertions, SystemdPlugin::onNodeStarted, "READY=1"); } @@ -109,34 +137,34 @@ public void testCloseSuccess() { runTestClose( Boolean.TRUE.toString(), randomIntBetween(1, Integer.MAX_VALUE), - maybe -> assertThat(maybe, OptionalMatchers.isEmpty())); + (maybe, plugin) -> assertThat(maybe, OptionalMatchers.isEmpty())); } public void testCloseFailure() { runTestClose( Boolean.TRUE.toString(), randomIntBetween(Integer.MIN_VALUE, -1), - maybe -> assertThat(maybe, OptionalMatchers.isEmpty())); + (maybe, plugin) -> assertThat(maybe, OptionalMatchers.isEmpty())); } public void testCloseNotEnabled() { runTestClose( Boolean.FALSE.toString(), randomInt(), - maybe -> assertThat(maybe, OptionalMatchers.isEmpty())); + (maybe, plugin) -> assertThat(maybe, OptionalMatchers.isEmpty())); } private void runTestClose( final String esSDNotify, final int rc, - final Consumer> assertions) { + final BiConsumer, SystemdPlugin> assertions) { runTest(esSDNotify, rc, assertions, SystemdPlugin::close, "STOPPING=1"); } private void runTest( final String esSDNotify, final int rc, - final Consumer> assertions, + final BiConsumer, SystemdPlugin> assertions, final CheckedConsumer invocation, final String expectedState) { final AtomicBoolean invoked = new AtomicBoolean(); @@ -153,16 +181,22 @@ int sd_notify(final int unset_environment, final String state) { } }; + plugin.createComponents(null, null, threadPool, null, null, null, null, null, null); + if (Boolean.TRUE.toString().equals(esSDNotify)) { + assertNotNull(plugin.extender); + } else { + assertNull(plugin.extender); + } boolean success = false; try { invocation.accept(plugin); success = true; } catch (final Exception e) { - assertions.accept(Optional.of(e)); + assertions.accept(Optional.of(e), plugin); } if (success) { - assertions.accept(Optional.empty()); + assertions.accept(Optional.empty(), plugin); } if (Boolean.TRUE.toString().equals(esSDNotify)) { assertTrue(invoked.get()); From f73bc2d307cea851b407a0e8310d58321df53266 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 3 Dec 2019 22:44:20 +0100 Subject: [PATCH 056/686] Stop Copying Every Http Request in Message Handler (#44564) * Copying the request is not necessary here. We can simply release it once the response has been generated and a lot of `Unpooled` allocations that way * Relates #32228 * I think the issue that preventet that PR that PR from being merged was solved by #39634 that moved the bulk index marker search to ByteBuf bulk access so the composite buffer shouldn't require many additional bounds checks (I'd argue the bounds checks we add, we save when copying the composite buffer) * I couldn't neccessarily reproduce much of a speedup from this change, but I could reproduce a very measureable reduction in GC time with e.g. Rally's PMC (4g heap node and bulk requests of size 5k saw a reduction in young GC time by ~10% for me) --- .../http/netty4/Netty4HttpRequest.java | 53 +++++++++++++++---- .../http/netty4/Netty4HttpRequestHandler.java | 41 ++++++-------- .../http/nio/NioHttpRequest.java | 9 ++++ .../http/DefaultRestChannel.java | 3 +- .../org/elasticsearch/http/HttpRequest.java | 12 +++++ .../elasticsearch/rest/RestController.java | 4 ++ .../org/elasticsearch/rest/RestHandler.java | 12 +++++ .../org/elasticsearch/rest/RestRequest.java | 12 ++++- .../rest/action/document/RestBulkAction.java | 5 ++ .../rest/action/search/RestSearchAction.java | 5 ++ .../http/DefaultRestChannelTests.java | 9 ++++ .../rest/RestControllerTests.java | 9 ++++ .../test/rest/FakeRestRequest.java | 9 ++++ .../security/rest/SecurityRestFilter.java | 5 ++ 14 files changed, 151 insertions(+), 37 deletions(-) diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequest.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequest.java index ffabe5cbbe224..e0ad3007c98a9 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequest.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequest.java @@ -19,6 +19,8 @@ package org.elasticsearch.http.netty4; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.FullHttpRequest; @@ -28,7 +30,6 @@ import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; import io.netty.handler.codec.http.cookie.ServerCookieEncoder; -import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.http.HttpRequest; import org.elasticsearch.rest.RestRequest; @@ -41,23 +42,30 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; public class Netty4HttpRequest implements HttpRequest { - private final FullHttpRequest request; - private final BytesReference content; private final HttpHeadersMap headers; private final int sequence; + private final AtomicBoolean released; + private final FullHttpRequest request; + private final boolean pooled; + private final BytesReference content; Netty4HttpRequest(FullHttpRequest request, int sequence) { + this(request, new HttpHeadersMap(request.headers()), sequence, new AtomicBoolean(false), true, + Netty4Utils.toBytesReference(request.content())); + } + + private Netty4HttpRequest(FullHttpRequest request, HttpHeadersMap headers, int sequence, AtomicBoolean released, boolean pooled, + BytesReference content) { this.request = request; - headers = new HttpHeadersMap(request.headers()); this.sequence = sequence; - if (request.content().isReadable()) { - this.content = Netty4Utils.toBytesReference(request.content()); - } else { - this.content = BytesArray.EMPTY; - } + this.headers = headers; + this.content = content; + this.pooled = pooled; + this.released = released; } @Override @@ -105,9 +113,33 @@ public String uri() { @Override public BytesReference content() { + assert released.get() == false; return content; } + @Override + public void release() { + if (pooled && released.compareAndSet(false, true)) { + request.release(); + } + } + + @Override + public HttpRequest releaseAndCopy() { + assert released.get() == false; + if (pooled == false) { + return this; + } + try { + final ByteBuf copiedContent = Unpooled.copiedBuffer(request.content()); + return new Netty4HttpRequest( + new DefaultFullHttpRequest(request.protocolVersion(), request.method(), request.uri(), copiedContent, request.headers(), + request.trailingHeaders()), + headers, sequence, new AtomicBoolean(false), false, Netty4Utils.toBytesReference(copiedContent)); + } finally { + release(); + } + } @Override public final Map> getHeaders() { @@ -147,7 +179,8 @@ public HttpRequest removeHeader(String header) { trailingHeaders.remove(header); FullHttpRequest requestWithoutHeader = new DefaultFullHttpRequest(request.protocolVersion(), request.method(), request.uri(), request.content(), headersWithoutContentTypeHeader, trailingHeaders); - return new Netty4HttpRequest(requestWithoutHeader, sequence); + return new Netty4HttpRequest(requestWithoutHeader, new HttpHeadersMap(requestWithoutHeader.headers()), sequence, released, + pooled, content); } @Override diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequestHandler.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequestHandler.java index ad6d84dfcb499..7e7f45ef92e2e 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequestHandler.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequestHandler.java @@ -19,11 +19,9 @@ package org.elasticsearch.http.netty4; -import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.http.HttpPipelinedRequest; @@ -41,32 +39,25 @@ class Netty4HttpRequestHandler extends SimpleChannelInboundHandler msg) { Netty4HttpChannel channel = ctx.channel().attr(Netty4HttpServerTransport.HTTP_CHANNEL_KEY).get(); FullHttpRequest request = msg.getRequest(); - final FullHttpRequest copiedRequest; + boolean success = false; + Netty4HttpRequest httpRequest = new Netty4HttpRequest(request, msg.getSequence()); try { - copiedRequest = - new DefaultFullHttpRequest( - request.protocolVersion(), - request.method(), - request.uri(), - Unpooled.copiedBuffer(request.content()), - request.headers(), - request.trailingHeaders()); - } finally { - // As we have copied the buffer, we can release the request - request.release(); - } - Netty4HttpRequest httpRequest = new Netty4HttpRequest(copiedRequest, msg.getSequence()); - - if (request.decoderResult().isFailure()) { - Throwable cause = request.decoderResult().cause(); - if (cause instanceof Error) { - ExceptionsHelper.maybeDieOnAnotherThread(cause); - serverTransport.incomingRequestError(httpRequest, channel, new Exception(cause)); + if (request.decoderResult().isFailure()) { + Throwable cause = request.decoderResult().cause(); + if (cause instanceof Error) { + ExceptionsHelper.maybeDieOnAnotherThread(cause); + serverTransport.incomingRequestError(httpRequest, channel, new Exception(cause)); + } else { + serverTransport.incomingRequestError(httpRequest, channel, (Exception) cause); + } } else { - serverTransport.incomingRequestError(httpRequest, channel, (Exception) cause); + serverTransport.incomingRequest(httpRequest, channel); + } + success = true; + } finally { + if (success == false) { + httpRequest.release(); } - } else { - serverTransport.incomingRequest(httpRequest, channel); } } diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpRequest.java b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpRequest.java index 08937593f3ba6..8e17d37699cdd 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpRequest.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpRequest.java @@ -108,6 +108,15 @@ public BytesReference content() { return content; } + @Override + public void release() { + // NioHttpRequest works from copied unpooled bytes no need to release anything + } + + @Override + public HttpRequest releaseAndCopy() { + return this; + } @Override public final Map> getHeaders() { diff --git a/server/src/main/java/org/elasticsearch/http/DefaultRestChannel.java b/server/src/main/java/org/elasticsearch/http/DefaultRestChannel.java index 098a01410897c..98f578bb3412c 100644 --- a/server/src/main/java/org/elasticsearch/http/DefaultRestChannel.java +++ b/server/src/main/java/org/elasticsearch/http/DefaultRestChannel.java @@ -77,7 +77,8 @@ protected BytesStreamOutput newBytesOutput() { @Override public void sendResponse(RestResponse restResponse) { - final ArrayList toClose = new ArrayList<>(3); + final ArrayList toClose = new ArrayList<>(4); + toClose.add(httpRequest::release); if (isCloseConnection()) { toClose.add(() -> CloseableChannel.closeChannel(httpChannel)); } diff --git a/server/src/main/java/org/elasticsearch/http/HttpRequest.java b/server/src/main/java/org/elasticsearch/http/HttpRequest.java index 02a3a58d1702d..4d67078fe571a 100644 --- a/server/src/main/java/org/elasticsearch/http/HttpRequest.java +++ b/server/src/main/java/org/elasticsearch/http/HttpRequest.java @@ -68,4 +68,16 @@ enum HttpVersion { */ HttpResponse createResponse(RestStatus status, BytesReference content); + /** + * Release any resources associated with this request. Implementations should be idempotent. The behavior of {@link #content()} + * after this method has been invoked is undefined and implementation specific. + */ + void release(); + + /** + * If this instances uses any pooled resources, creates a copy of this instance that does not use any pooled resources and releases + * any resources associated with this instance. If the instance does not use any shared resources, returns itself. + * @return a safe unpooled http request + */ + HttpRequest releaseAndCopy(); } diff --git a/server/src/main/java/org/elasticsearch/rest/RestController.java b/server/src/main/java/org/elasticsearch/rest/RestController.java index 1c174c89df608..aa39ccdc4659c 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestController.java +++ b/server/src/main/java/org/elasticsearch/rest/RestController.java @@ -218,6 +218,10 @@ private void dispatchRequest(RestRequest request, RestChannel channel, RestHandl } // iff we could reserve bytes for the request we need to send the response also over this channel responseChannel = new ResourceHandlingHttpChannel(channel, circuitBreakerService, contentLength); + // TODO: Count requests double in the circuit breaker if they need copying? + if (handler.allowsUnsafeBuffers() == false) { + request.ensureSafeBuffers(); + } handler.handleRequest(request, responseChannel, client); } catch (Exception e) { responseChannel.sendResponse(new BytesRestResponse(responseChannel, e)); diff --git a/server/src/main/java/org/elasticsearch/rest/RestHandler.java b/server/src/main/java/org/elasticsearch/rest/RestHandler.java index 1ebc7a7fd1bd2..605dd41078a54 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestHandler.java +++ b/server/src/main/java/org/elasticsearch/rest/RestHandler.java @@ -47,4 +47,16 @@ default boolean canTripCircuitBreaker() { default boolean supportsContentStream() { return false; } + + /** + * Indicates if the RestHandler supports working with pooled buffers. If the request handler will not escape the return + * {@link RestRequest#content()} or any buffers extracted from it then there is no need to make a copies of any pooled buffers in the + * {@link RestRequest} instance before passing a request to this handler. If this instance does not support pooled/unsafe buffers + * {@link RestRequest#ensureSafeBuffers()} should be called on any request before passing it to {@link #handleRequest}. + * + * @return true iff the handler supports requests that make use of pooled buffers + */ + default boolean allowsUnsafeBuffers() { + return false; + } } diff --git a/server/src/main/java/org/elasticsearch/rest/RestRequest.java b/server/src/main/java/org/elasticsearch/rest/RestRequest.java index 23e72d0c1f1d5..4a8fb44fc4299 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestRequest.java +++ b/server/src/main/java/org/elasticsearch/rest/RestRequest.java @@ -64,9 +64,10 @@ public class RestRequest implements ToXContent.Params { private final String rawPath; private final Set consumedParams = new HashSet<>(); private final SetOnce xContentType = new SetOnce<>(); - private final HttpRequest httpRequest; private final HttpChannel httpChannel; + private HttpRequest httpRequest; + private boolean contentConsumed = false; public boolean isContentConsumed() { @@ -97,6 +98,15 @@ protected RestRequest(RestRequest restRequest) { restRequest.getHttpRequest(), restRequest.getHttpChannel()); } + /** + * Invoke {@link HttpRequest#releaseAndCopy()} on the http request in this instance and replace a pooled http request + * with an unpooled copy. This is supposed to be used before passing requests to {@link RestHandler} instances that can not safely + * handle http requests that use pooled buffers as determined by {@link RestHandler#allowsUnsafeBuffers()}. + */ + void ensureSafeBuffers() { + httpRequest = httpRequest.releaseAndCopy(); + } + /** * Creates a new REST request. This method will throw {@link BadParameterException} if the path cannot be * decoded diff --git a/server/src/main/java/org/elasticsearch/rest/action/document/RestBulkAction.java b/server/src/main/java/org/elasticsearch/rest/action/document/RestBulkAction.java index 43cd684bd47a7..db925cd663ff9 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/document/RestBulkAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/document/RestBulkAction.java @@ -86,4 +86,9 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC public boolean supportsContentStream() { return true; } + + @Override + public boolean allowsUnsafeBuffers() { + return true; + } } diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java index c68c31473d1bc..11dc9f89de532 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java @@ -310,4 +310,9 @@ public static void checkRestTotalHits(RestRequest restRequest, SearchRequest sea protected Set responseParams() { return RESPONSE_PARAMS; } + + @Override + public boolean allowsUnsafeBuffers() { + return true; + } } diff --git a/server/src/test/java/org/elasticsearch/http/DefaultRestChannelTests.java b/server/src/test/java/org/elasticsearch/http/DefaultRestChannelTests.java index 85670e893b970..d671b81a09bc9 100644 --- a/server/src/test/java/org/elasticsearch/http/DefaultRestChannelTests.java +++ b/server/src/test/java/org/elasticsearch/http/DefaultRestChannelTests.java @@ -460,6 +460,15 @@ public HttpRequest removeHeader(String header) { public HttpResponse createResponse(RestStatus status, BytesReference content) { return new TestResponse(status, content); } + + @Override + public void release() { + } + + @Override + public HttpRequest releaseAndCopy() { + return this; + } } private static class TestResponse implements HttpResponse { diff --git a/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java b/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java index eb3d76bc82ae2..8413864d2ea26 100644 --- a/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java +++ b/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java @@ -608,6 +608,15 @@ public HttpRequest removeHeader(String header) { public HttpResponse createResponse(RestStatus status, BytesReference content) { return null; } + + @Override + public void release() { + } + + @Override + public HttpRequest releaseAndCopy() { + return this; + } }, null); final AssertingChannel channel = new AssertingChannel(request, true, RestStatus.METHOD_NOT_ALLOWED); diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/FakeRestRequest.java b/test/framework/src/main/java/org/elasticsearch/test/rest/FakeRestRequest.java index a659d6af5c6aa..2f2f5fb76bfe7 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/FakeRestRequest.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/FakeRestRequest.java @@ -113,6 +113,15 @@ public boolean containsHeader(String name) { } }; } + + @Override + public void release() { + } + + @Override + public HttpRequest releaseAndCopy() { + return this; + } } private static class FakeHttpChannel implements HttpChannel { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java index df678f9c63ba4..4131d1e735883 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java @@ -86,6 +86,11 @@ public boolean supportsContentStream() { return restHandler.supportsContentStream(); } + @Override + public boolean allowsUnsafeBuffers() { + return restHandler.allowsUnsafeBuffers(); + } + private RestRequest maybeWrapRestRequest(RestRequest restRequest) throws IOException { if (restHandler instanceof RestRequestFilter) { return ((RestRequestFilter)restHandler).getFilteredRequest(restRequest); From a3a8831ef3f054dc7c6e36ec503feda24b18fb85 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Tue, 3 Dec 2019 16:13:56 -0800 Subject: [PATCH 057/686] Update CI BWC versions --- .ci/bwcVersions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/bwcVersions b/.ci/bwcVersions index 621d2e0b489dc..52b778bf11235 100644 --- a/.ci/bwcVersions +++ b/.ci/bwcVersions @@ -11,7 +11,7 @@ BWC_VERSION: - "7.4.0" - "7.4.1" - "7.4.2" - - "7.4.3" - "7.5.0" + - "7.5.1" - "7.6.0" - "8.0.0" From f4368c3f9d04f2e643987dca3f148923fac065ea Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Tue, 3 Dec 2019 18:55:48 -0700 Subject: [PATCH 058/686] Scripting: add available languages & contexts API (#49652) Adds `GET /_script_language` to support Kibana dynamic scripting language selection. Response contains whether `inline` and/or `stored` scripts are enabled as determined by the `script.allowed_types` settings. For each scripting language registered, such as `painless`, `expression`, `mustache` or custom, available contexts for the language are included as determined by the `script.allowed_contexts` setting. Response format: ``` { "types_allowed": [ "inline", "stored" ], "language_contexts": [ { "language": "expression", "contexts": [ "aggregation_selector", "aggs" ... ] }, { "language": "painless", "contexts": [ "aggregation_selector", "aggs", "aggs_combine", ... ] } ... ] } ``` Fixes: #49463 --- .../client/RestHighLevelClientTests.java | 1 + .../expression/ExpressionScriptEngine.java | 89 +++++---- .../script/mustache/MustacheScriptEngine.java | 6 + .../painless/PainlessScriptEngine.java | 5 + .../expertscript/ExpertScriptPlugin.java | 6 + .../api/get_script_languages.json | 19 ++ .../test/scripts/25_get_script_languages.yml | 9 + .../elasticsearch/action/ActionModule.java | 5 + .../GetScriptLanguageAction.java | 31 ++++ .../GetScriptLanguageRequest.java | 42 +++++ .../GetScriptLanguageResponse.java | 78 ++++++++ .../TransportGetScriptLanguageAction.java | 43 +++++ .../cluster/RestGetScriptLanguageAction.java | 51 ++++++ .../elasticsearch/script/ScriptEngine.java | 6 + .../script/ScriptLanguagesInfo.java | 170 ++++++++++++++++++ .../elasticsearch/script/ScriptService.java | 22 +++ .../GetScriptLanguageResponseTests.java | 136 ++++++++++++++ .../script/ScriptLanguagesInfoTests.java | 134 ++++++++++++++ .../functionscore/ExplainableScriptIT.java | 6 + .../search/suggest/SuggestSearchIT.java | 6 + .../script/MockScriptEngine.java | 30 ++++ 21 files changed, 858 insertions(+), 37 deletions(-) create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/api/get_script_languages.json create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/test/scripts/25_get_script_languages.yml create mode 100644 server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageAction.java create mode 100644 server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageRequest.java create mode 100644 server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageResponse.java create mode 100644 server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/TransportGetScriptLanguageAction.java create mode 100644 server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetScriptLanguageAction.java create mode 100644 server/src/main/java/org/elasticsearch/script/ScriptLanguagesInfo.java create mode 100644 server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageResponseTests.java create mode 100644 server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index 598ccce0f33e0..3135239530199 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -765,6 +765,7 @@ public void testApiNamingConventions() throws Exception { "cluster.remote_info", "create", "get_script_context", + "get_script_languages", "get_source", "indices.exists_type", "indices.get_upgrade", diff --git a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java index afe507279027d..3df42b53cbb34 100644 --- a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java +++ b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java @@ -55,6 +55,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Function; /** * Provides the infrastructure for Lucene expressions as a scripting language for Elasticsearch. @@ -65,6 +67,41 @@ public class ExpressionScriptEngine implements ScriptEngine { public static final String NAME = "expression"; + private static Map, Function> contexts = Map.of( + BucketAggregationScript.CONTEXT, + ExpressionScriptEngine::newBucketAggregationScriptFactory, + + BucketAggregationSelectorScript.CONTEXT, + (Expression expr) -> { + BucketAggregationScript.Factory factory = newBucketAggregationScriptFactory(expr); + BucketAggregationSelectorScript.Factory wrappedFactory = parameters -> new BucketAggregationSelectorScript(parameters) { + @Override + public boolean execute() { + return factory.newInstance(getParams()).execute().doubleValue() == 1.0; + } + }; + return wrappedFactory; + }, + + FilterScript.CONTEXT, + (Expression expr) -> (FilterScript.Factory) (p, lookup) -> newFilterScript(expr, lookup, p), + + ScoreScript.CONTEXT, + (Expression expr) -> (ScoreScript.Factory) (p, lookup) -> newScoreScript(expr, lookup, p), + + TermsSetQueryScript.CONTEXT, + (Expression expr) -> (TermsSetQueryScript.Factory) (p, lookup) -> newTermsSetQueryScript(expr, lookup, p), + + AggregationScript.CONTEXT, + (Expression expr) -> (AggregationScript.Factory) (p, lookup) -> newAggregationScript(expr, lookup, p), + + NumberSortScript.CONTEXT, + (Expression expr) -> (NumberSortScript.Factory) (p, lookup) -> newSortScript(expr, lookup, p), + + FieldScript.CONTEXT, + (Expression expr) -> (FieldScript.Factory) (p, lookup) -> newFieldScript(expr, lookup, p) + ); + @Override public String getType() { return NAME; @@ -102,37 +139,15 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE } } }); - if (context.instanceClazz.equals(BucketAggregationScript.class)) { - return context.factoryClazz.cast(newBucketAggregationScriptFactory(expr)); - } else if (context.instanceClazz.equals(BucketAggregationSelectorScript.class)) { - BucketAggregationScript.Factory factory = newBucketAggregationScriptFactory(expr); - BucketAggregationSelectorScript.Factory wrappedFactory = parameters -> new BucketAggregationSelectorScript(parameters) { - @Override - public boolean execute() { - return factory.newInstance(getParams()).execute().doubleValue() == 1.0; - } - }; - return context.factoryClazz.cast(wrappedFactory); - } else if (context.instanceClazz.equals(FilterScript.class)) { - FilterScript.Factory factory = (p, lookup) -> newFilterScript(expr, lookup, p); - return context.factoryClazz.cast(factory); - } else if (context.instanceClazz.equals(ScoreScript.class)) { - ScoreScript.Factory factory = (p, lookup) -> newScoreScript(expr, lookup, p); - return context.factoryClazz.cast(factory); - } else if (context.instanceClazz.equals(TermsSetQueryScript.class)) { - TermsSetQueryScript.Factory factory = (p, lookup) -> newTermsSetQueryScript(expr, lookup, p); - return context.factoryClazz.cast(factory); - } else if (context.instanceClazz.equals(AggregationScript.class)) { - AggregationScript.Factory factory = (p, lookup) -> newAggregationScript(expr, lookup, p); - return context.factoryClazz.cast(factory); - } else if (context.instanceClazz.equals(NumberSortScript.class)) { - NumberSortScript.Factory factory = (p, lookup) -> newSortScript(expr, lookup, p); - return context.factoryClazz.cast(factory); - } else if (context.instanceClazz.equals(FieldScript.class)) { - FieldScript.Factory factory = (p, lookup) -> newFieldScript(expr, lookup, p); - return context.factoryClazz.cast(factory); + if (contexts.containsKey(context) == false) { + throw new IllegalArgumentException("expression engine does not know how to handle script context [" + context.name + "]"); } - throw new IllegalArgumentException("expression engine does not know how to handle script context [" + context.name + "]"); + return context.factoryClazz.cast(contexts.get(context).apply(expr)); + } + + @Override + public Set> getSupportedContexts() { + return contexts.keySet(); } private static BucketAggregationScript.Factory newBucketAggregationScriptFactory(Expression expr) { @@ -166,7 +181,7 @@ public Double execute() { }; } - private NumberSortScript.LeafFactory newSortScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { + private static NumberSortScript.LeafFactory newSortScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { // NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings, // instead of complicating SimpleBindings (which should stay simple) SimpleBindings bindings = new SimpleBindings(); @@ -193,7 +208,7 @@ private NumberSortScript.LeafFactory newSortScript(Expression expr, SearchLookup return new ExpressionNumberSortScript(expr, bindings, needsScores); } - private TermsSetQueryScript.LeafFactory newTermsSetQueryScript(Expression expr, SearchLookup lookup, + private static TermsSetQueryScript.LeafFactory newTermsSetQueryScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { // NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings, // instead of complicating SimpleBindings (which should stay simple) @@ -216,7 +231,7 @@ private TermsSetQueryScript.LeafFactory newTermsSetQueryScript(Expression expr, return new ExpressionTermSetQueryScript(expr, bindings); } - private AggregationScript.LeafFactory newAggregationScript(Expression expr, SearchLookup lookup, + private static AggregationScript.LeafFactory newAggregationScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { // NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings, // instead of complicating SimpleBindings (which should stay simple) @@ -252,7 +267,7 @@ private AggregationScript.LeafFactory newAggregationScript(Expression expr, Sear return new ExpressionAggregationScript(expr, bindings, needsScores, specialValue); } - private FieldScript.LeafFactory newFieldScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { + private static FieldScript.LeafFactory newFieldScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { SimpleBindings bindings = new SimpleBindings(); for (String variable : expr.variables) { try { @@ -273,7 +288,7 @@ private FieldScript.LeafFactory newFieldScript(Expression expr, SearchLookup loo * This is a hack for filter scripts, which must return booleans instead of doubles as expression do. * See https://github.com/elastic/elasticsearch/issues/26429. */ - private FilterScript.LeafFactory newFilterScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { + private static FilterScript.LeafFactory newFilterScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { ScoreScript.LeafFactory searchLeafFactory = newScoreScript(expr, lookup, vars); return ctx -> { ScoreScript script = searchLeafFactory.newInstance(ctx); @@ -290,7 +305,7 @@ public void setDocument(int docid) { }; } - private ScoreScript.LeafFactory newScoreScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { + private static ScoreScript.LeafFactory newScoreScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { // NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings, // instead of complicating SimpleBindings (which should stay simple) SimpleBindings bindings = new SimpleBindings(); @@ -327,7 +342,7 @@ private ScoreScript.LeafFactory newScoreScript(Expression expr, SearchLookup loo /** * converts a ParseException at compile-time or link-time to a ScriptException */ - private ScriptException convertToScriptException(String message, String source, String portion, Throwable cause) { + private static ScriptException convertToScriptException(String message, String source, String portion, Throwable cause) { List stack = new ArrayList<>(); stack.add(portion); StringBuilder pointer = new StringBuilder(); diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java index ca28d12a7bda5..f453905089fdd 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java @@ -41,6 +41,7 @@ import java.security.PrivilegedAction; import java.util.Collections; import java.util.Map; +import java.util.Set; /** * Main entry point handling template registration, compilation and @@ -79,6 +80,11 @@ public T compile(String templateName, String templateSource, ScriptContext> getSupportedContexts() { + return Set.of(TemplateScript.CONTEXT); + } + private CustomMustacheFactory createMustacheFactory(Map options) { if (options == null || options.isEmpty() || options.containsKey(Script.CONTENT_TYPE_OPTION) == false) { return new CustomMustacheFactory(); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java index 11bfbe3b40fc6..7f64e992bc122 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java @@ -146,6 +146,11 @@ public Loader run() { } } + @Override + public Set> getSupportedContexts() { + return contextsToCompilers.keySet(); + } + /** * Generates a stateful factory class that will return script instances. Acts as a middle man between * the {@link ScriptContext#factoryClazz} and the {@link ScriptContext#instanceClazz} when used so that diff --git a/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java b/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java index 0b65084dee466..5259d32a2837b 100644 --- a/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java +++ b/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java @@ -35,6 +35,7 @@ import java.io.UncheckedIOException; import java.util.Collection; import java.util.Map; +import java.util.Set; /** * An example script plugin that adds a {@link ScriptEngine} implementing expert scoring. @@ -76,6 +77,11 @@ public void close() { // optionally close resources } + @Override + public Set> getSupportedContexts() { + return Set.of(ScoreScript.CONTEXT); + } + private static class PureDfLeafFactory implements LeafFactory { private final Map params; private final SearchLookup lookup; diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/get_script_languages.json b/rest-api-spec/src/main/resources/rest-api-spec/api/get_script_languages.json new file mode 100644 index 0000000000000..5a45392d9ee11 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/get_script_languages.json @@ -0,0 +1,19 @@ +{ + "get_script_languages":{ + "documentation":{ + "description":"Returns available script types, languages and contexts" + }, + "stability":"experimental", + "url":{ + "paths":[ + { + "path":"/_script_language", + "methods":[ + "GET" + ] + } + ] + }, + "params":{} + } +} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/scripts/25_get_script_languages.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/scripts/25_get_script_languages.yml new file mode 100644 index 0000000000000..f4d764324e2dd --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/scripts/25_get_script_languages.yml @@ -0,0 +1,9 @@ +"Action to get script languages": + - skip: + version: " - 7.6.0" + reason: "get_script_languages introduced in 7.6.0" + - do: + get_script_languages: {} + + - match: { types_allowed.0: "inline" } + - match: { types_allowed.1: "stored" } diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 10dcf6943f867..3d60a1fb698d2 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -80,10 +80,12 @@ import org.elasticsearch.action.admin.cluster.stats.TransportClusterStatsAction; import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptAction; import org.elasticsearch.action.admin.cluster.storedscripts.GetScriptContextAction; +import org.elasticsearch.action.admin.cluster.storedscripts.GetScriptLanguageAction; import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptAction; import org.elasticsearch.action.admin.cluster.storedscripts.PutStoredScriptAction; import org.elasticsearch.action.admin.cluster.storedscripts.TransportDeleteStoredScriptAction; import org.elasticsearch.action.admin.cluster.storedscripts.TransportGetScriptContextAction; +import org.elasticsearch.action.admin.cluster.storedscripts.TransportGetScriptLanguageAction; import org.elasticsearch.action.admin.cluster.storedscripts.TransportGetStoredScriptAction; import org.elasticsearch.action.admin.cluster.storedscripts.TransportPutStoredScriptAction; import org.elasticsearch.action.admin.cluster.tasks.PendingClusterTasksAction; @@ -249,6 +251,7 @@ import org.elasticsearch.rest.action.admin.cluster.RestDeleteStoredScriptAction; import org.elasticsearch.rest.action.admin.cluster.RestGetRepositoriesAction; import org.elasticsearch.rest.action.admin.cluster.RestGetScriptContextAction; +import org.elasticsearch.rest.action.admin.cluster.RestGetScriptLanguageAction; import org.elasticsearch.rest.action.admin.cluster.RestGetSnapshotsAction; import org.elasticsearch.rest.action.admin.cluster.RestGetStoredScriptAction; import org.elasticsearch.rest.action.admin.cluster.RestGetTaskAction; @@ -522,6 +525,7 @@ public void reg actions.register(GetStoredScriptAction.INSTANCE, TransportGetStoredScriptAction.class); actions.register(DeleteStoredScriptAction.INSTANCE, TransportDeleteStoredScriptAction.class); actions.register(GetScriptContextAction.INSTANCE, TransportGetScriptContextAction.class); + actions.register(GetScriptLanguageAction.INSTANCE, TransportGetScriptLanguageAction.class); actions.register(FieldCapabilitiesAction.INSTANCE, TransportFieldCapabilitiesAction.class); actions.register(TransportFieldCapabilitiesIndexAction.TYPE, TransportFieldCapabilitiesIndexAction.class); @@ -662,6 +666,7 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestPutStoredScriptAction(restController)); registerHandler.accept(new RestDeleteStoredScriptAction(restController)); registerHandler.accept(new RestGetScriptContextAction(restController)); + registerHandler.accept(new RestGetScriptLanguageAction(restController)); registerHandler.accept(new RestFieldCapabilitiesAction(restController)); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageAction.java new file mode 100644 index 0000000000000..d4c6ae2de052c --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageAction.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package org.elasticsearch.action.admin.cluster.storedscripts; + +import org.elasticsearch.action.ActionType; + +public class GetScriptLanguageAction extends ActionType { + public static final GetScriptLanguageAction INSTANCE = new GetScriptLanguageAction(); + public static final String NAME = "cluster:admin/script_language/get"; + + private GetScriptLanguageAction() { + super(NAME, GetScriptLanguageResponse::new); + } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageRequest.java new file mode 100644 index 0000000000000..c5433be2febfa --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageRequest.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package org.elasticsearch.action.admin.cluster.storedscripts; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; + +public class GetScriptLanguageRequest extends ActionRequest { + public GetScriptLanguageRequest() { + super(); + } + + GetScriptLanguageRequest(StreamInput in) throws IOException { + super(in); + } + + @Override + public ActionRequestValidationException validate() { return null; } + + @Override + public String toString() { return "get script languages"; } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageResponse.java new file mode 100644 index 0000000000000..7d8ea7654c4d4 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageResponse.java @@ -0,0 +1,78 @@ +/* + * 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. + */ + +package org.elasticsearch.action.admin.cluster.storedscripts; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.StatusToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.script.ScriptLanguagesInfo; + +import java.io.IOException; +import java.util.Objects; + +public class GetScriptLanguageResponse extends ActionResponse implements StatusToXContentObject, Writeable { + public final ScriptLanguagesInfo info; + + GetScriptLanguageResponse(ScriptLanguagesInfo info) { + this.info = info; + } + + GetScriptLanguageResponse(StreamInput in) throws IOException { + super(in); + info = new ScriptLanguagesInfo(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + info.writeTo(out); + } + + @Override + public RestStatus status() { + return RestStatus.OK; + } + + public static GetScriptLanguageResponse fromXContent(XContentParser parser) throws IOException { + return new GetScriptLanguageResponse(ScriptLanguagesInfo.fromXContent(parser)); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + GetScriptLanguageResponse that = (GetScriptLanguageResponse) o; + return info.equals(that.info); + } + + @Override + public int hashCode() { return Objects.hash(info); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return info.toXContent(builder, params); + } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/TransportGetScriptLanguageAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/TransportGetScriptLanguageAction.java new file mode 100644 index 0000000000000..96f07de533c25 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/TransportGetScriptLanguageAction.java @@ -0,0 +1,43 @@ +/* + * 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. + */ + +package org.elasticsearch.action.admin.cluster.storedscripts; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; + +public class TransportGetScriptLanguageAction extends HandledTransportAction { + private final ScriptService scriptService; + + @Inject + public TransportGetScriptLanguageAction(TransportService transportService, ActionFilters actionFilters, ScriptService scriptService) { + super(GetScriptLanguageAction.NAME, transportService, actionFilters, GetScriptLanguageRequest::new); + this.scriptService = scriptService; + } + + @Override + protected void doExecute(Task task, GetScriptLanguageRequest request, ActionListener listener) { + listener.onResponse(new GetScriptLanguageResponse(scriptService.getScriptLanguages())); + } +} diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetScriptLanguageAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetScriptLanguageAction.java new file mode 100644 index 0000000000000..c9246b910cf4f --- /dev/null +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetScriptLanguageAction.java @@ -0,0 +1,51 @@ +/* + * 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. + */ + +package org.elasticsearch.rest.action.admin.cluster; + +import org.elasticsearch.action.admin.cluster.storedscripts.GetScriptLanguageAction; +import org.elasticsearch.action.admin.cluster.storedscripts.GetScriptLanguageRequest; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; + +import java.io.IOException; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +public class RestGetScriptLanguageAction extends BaseRestHandler { + @Inject + public RestGetScriptLanguageAction(RestController controller) { + controller.registerHandler(GET, "/_script_language", this); + } + + @Override public String getName() { + return "script_language_action"; + } + + @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + return channel -> client.execute(GetScriptLanguageAction.INSTANCE, + new GetScriptLanguageRequest(), + new RestToXContentListener<>(channel)); + } + +} diff --git a/server/src/main/java/org/elasticsearch/script/ScriptEngine.java b/server/src/main/java/org/elasticsearch/script/ScriptEngine.java index bd32cce0b3781..9ace06d701d14 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptEngine.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptEngine.java @@ -22,6 +22,7 @@ import java.io.Closeable; import java.io.IOException; import java.util.Map; +import java.util.Set; /** * A script language implementation. @@ -45,4 +46,9 @@ public interface ScriptEngine extends Closeable { @Override default void close() throws IOException {} + + /** + * Script contexts supported by this engine. + */ + Set> getSupportedContexts(); } diff --git a/server/src/main/java/org/elasticsearch/script/ScriptLanguagesInfo.java b/server/src/main/java/org/elasticsearch/script/ScriptLanguagesInfo.java new file mode 100644 index 0000000000000..d8bfb4f499fe5 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/ScriptLanguagesInfo.java @@ -0,0 +1,170 @@ +/* + * 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. + */ + +package org.elasticsearch.script; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +/** + * The allowable types, languages and their corresponding contexts. When serialized there is a top level types_allowed list, + * meant to reflect the setting script.allowed_types with the allowed types (eg inline, stored). + * + * The top-level language_contexts list of objects have the language (eg. painless, + * mustache) and a list of contexts available for the language. It is the responsibility of the caller to ensure + * these contexts are filtered by the script.allowed_contexts setting. + * + * The json serialization of the object has the form: + * + * { + * "types_allowed": [ + * "inline", + * "stored" + * ], + * "language_contexts": [ + * { + * "language": "expression", + * "contexts": [ + * "aggregation_selector", + * "aggs" + * ... + * ] + * }, + * { + * "language": "painless", + * "contexts": [ + * "aggregation_selector", + * "aggs", + * "aggs_combine", + * ... + * ] + * } + * ... + * ] + * } + * + */ +public class ScriptLanguagesInfo implements ToXContentObject, Writeable { + private static final ParseField TYPES_ALLOWED = new ParseField("types_allowed"); + private static final ParseField LANGUAGE_CONTEXTS = new ParseField("language_contexts"); + private static final ParseField LANGUAGE = new ParseField("language"); + private static final ParseField CONTEXTS = new ParseField("contexts"); + + public final Set typesAllowed; + public final Map> languageContexts; + + public ScriptLanguagesInfo(Set typesAllowed, Map> languageContexts) { + this.typesAllowed = typesAllowed != null ? Set.copyOf(typesAllowed): Collections.emptySet(); + this.languageContexts = languageContexts != null ? Map.copyOf(languageContexts): Collections.emptyMap(); + } + + public ScriptLanguagesInfo(StreamInput in) throws IOException { + typesAllowed = in.readSet(StreamInput::readString); + languageContexts = in.readMap(StreamInput::readString, sin -> sin.readSet(StreamInput::readString)); + } + + @SuppressWarnings("unchecked") + public static ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("script_languages_info", true, + (a) -> new ScriptLanguagesInfo( + new HashSet<>((List)a[0]), + ((List>>)a[1]).stream().collect(Collectors.toMap(Tuple::v1, Tuple::v2)) + ) + ); + + @SuppressWarnings("unchecked") + private static ConstructingObjectParser>,Void> LANGUAGE_CONTEXT_PARSER = + new ConstructingObjectParser<>("language_contexts", true, + (m, name) -> new Tuple<>((String)m[0], Set.copyOf((List)m[1])) + ); + + static { + PARSER.declareStringArray(constructorArg(), TYPES_ALLOWED); + PARSER.declareObjectArray(constructorArg(), LANGUAGE_CONTEXT_PARSER, LANGUAGE_CONTEXTS); + LANGUAGE_CONTEXT_PARSER.declareString(constructorArg(), LANGUAGE); + LANGUAGE_CONTEXT_PARSER.declareStringArray(constructorArg(), CONTEXTS); + } + + public static ScriptLanguagesInfo fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeStringCollection(typesAllowed); + out.writeMap(languageContexts, StreamOutput::writeString, StreamOutput::writeStringCollection); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ScriptLanguagesInfo that = (ScriptLanguagesInfo) o; + return Objects.equals(typesAllowed, that.typesAllowed) && + Objects.equals(languageContexts, that.languageContexts); + } + + @Override + public int hashCode() { + return Objects.hash(typesAllowed, languageContexts); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject().startArray(TYPES_ALLOWED.getPreferredName()); + for (String type: typesAllowed.stream().sorted().collect(Collectors.toList())) { + builder.value(type); + } + + builder.endArray().startArray(LANGUAGE_CONTEXTS.getPreferredName()); + List>> languagesByName = languageContexts.entrySet().stream().sorted( + Map.Entry.comparingByKey() + ).collect(Collectors.toList()); + + for (Map.Entry> languageContext: languagesByName) { + builder.startObject().field(LANGUAGE.getPreferredName(), languageContext.getKey()).startArray(CONTEXTS.getPreferredName()); + for (String context: languageContext.getValue().stream().sorted().collect(Collectors.toList())) { + builder.value(context); + } + builder.endArray().endObject(); + } + + return builder.endArray().endObject(); + } +} diff --git a/server/src/main/java/org/elasticsearch/script/ScriptService.java b/server/src/main/java/org/elasticsearch/script/ScriptService.java index 799515256d302..a1788ee74a163 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptService.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptService.java @@ -52,12 +52,14 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; public class ScriptService implements Closeable, ClusterStateApplier { @@ -546,6 +548,26 @@ public Set getContextInfos() { return infos; } + public ScriptLanguagesInfo getScriptLanguages() { + Set types = typesAllowed; + if (types == null) { + types = new HashSet<>(); + for (ScriptType type: ScriptType.values()) { + types.add(type.getName()); + } + } + + final Set contexts = contextsAllowed != null ? contextsAllowed : this.contexts.keySet(); + Map> languageContexts = new HashMap<>(); + engines.forEach( + (key, value) -> languageContexts.put( + key, + value.getSupportedContexts().stream().map(c -> c.name).filter(contexts::contains).collect(Collectors.toSet()) + ) + ); + return new ScriptLanguagesInfo(types, languageContexts); + } + public ScriptStats stats() { return scriptMetrics.stats(); } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageResponseTests.java new file mode 100644 index 0000000000000..f330cb36b7296 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptLanguageResponseTests.java @@ -0,0 +1,136 @@ +/* + * 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. + */ + +package org.elasticsearch.action.admin.cluster.storedscripts; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.ScriptLanguagesInfo; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class GetScriptLanguageResponseTests extends AbstractSerializingTestCase { + private static int MAX_VALUES = 4; + private static final int MIN_LENGTH = 1; + private static final int MAX_LENGTH = 16; + + @Override + protected GetScriptLanguageResponse createTestInstance() { + if (randomBoolean()) { + return new GetScriptLanguageResponse( + new ScriptLanguagesInfo(Collections.emptySet(), Collections.emptyMap()) + ); + } + return new GetScriptLanguageResponse(randomInstance()); + } + + @Override + protected GetScriptLanguageResponse doParseInstance(XContentParser parser) throws IOException { + return GetScriptLanguageResponse.fromXContent(parser); + } + + @Override + protected Writeable.Reader instanceReader() { return GetScriptLanguageResponse::new; } + + @Override + protected GetScriptLanguageResponse mutateInstance(GetScriptLanguageResponse instance) throws IOException { + switch (randomInt(2)) { + case 0: + // mutate typesAllowed + return new GetScriptLanguageResponse( + new ScriptLanguagesInfo(mutateStringSet(instance.info.typesAllowed), instance.info.languageContexts) + ); + case 1: + // Add language + String language = randomValueOtherThanMany( + instance.info.languageContexts::containsKey, + () -> randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH) + ); + Map> languageContexts = new HashMap<>(); + instance.info.languageContexts.forEach(languageContexts::put); + languageContexts.put(language, randomStringSet(randomIntBetween(1, MAX_VALUES))); + return new GetScriptLanguageResponse(new ScriptLanguagesInfo(instance.info.typesAllowed, languageContexts)); + default: + // Mutate languageContexts + Map> lc = new HashMap<>(); + if (instance.info.languageContexts.size() == 0) { + lc.put(randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), randomStringSet(randomIntBetween(1, MAX_VALUES))); + } else { + int toModify = randomInt(instance.info.languageContexts.size()-1); + List keys = new ArrayList<>(instance.info.languageContexts.keySet()); + for (int i=0; i value = instance.info.languageContexts.get(keys.get(i)); + if (i == toModify) { + value = mutateStringSet(instance.info.languageContexts.get(keys.get(i))); + } + lc.put(key, value); + } + } + return new GetScriptLanguageResponse(new ScriptLanguagesInfo(instance.info.typesAllowed, lc)); + } + } + + private static ScriptLanguagesInfo randomInstance() { + Map> contexts = new HashMap<>(); + for (String context: randomStringSet(randomIntBetween(1, MAX_VALUES))) { + contexts.put(context, randomStringSet(randomIntBetween(1, MAX_VALUES))); + } + return new ScriptLanguagesInfo(randomStringSet(randomInt(MAX_VALUES)), contexts); + } + + private static Set randomStringSet(int numInstances) { + Set rand = new HashSet<>(numInstances); + for (int i = 0; i < numInstances; i++) { + rand.add(randomValueOtherThanMany(rand::contains, () -> randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH))); + } + return rand; + } + + private static Set mutateStringSet(Set strings) { + if (strings.isEmpty()) { + return Set.of(randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH)); + } + + if (randomBoolean()) { + Set updated = new HashSet<>(strings); + updated.add(randomValueOtherThanMany(updated::contains, () -> randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH))); + return updated; + } else { + List sorted = strings.stream().sorted().collect(Collectors.toList()); + int toRemove = randomInt(sorted.size() - 1); + Set updated = new HashSet<>(); + for (int i = 0; i < sorted.size(); i++) { + if (i != toRemove) { + updated.add(sorted.get(i)); + } + } + return updated; + } + } +} diff --git a/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java b/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java new file mode 100644 index 0000000000000..38139103ed2ab --- /dev/null +++ b/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java @@ -0,0 +1,134 @@ +/* + * 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. + */ + +package org.elasticsearch.script; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ScriptLanguagesInfoTests extends ESTestCase { + public void testEmptyTypesAllowedReturnsAllTypes() { + ScriptService ss = getMockScriptService(Settings.EMPTY); + ScriptLanguagesInfo info = ss.getScriptLanguages(); + ScriptType[] types = ScriptType.values(); + assertEquals(types.length, info.typesAllowed.size()); + for(ScriptType type: types) { + assertTrue("[" + type.getName() + "] is allowed", info.typesAllowed.contains(type.getName())); + } + } + + public void testSingleTypesAllowedReturnsThatType() { + for (ScriptType type: ScriptType.values()) { + ScriptService ss = getMockScriptService( + Settings.builder().put("script.allowed_types", type.getName()).build() + ); + ScriptLanguagesInfo info = ss.getScriptLanguages(); + assertEquals(1, info.typesAllowed.size()); + assertTrue("[" + type.getName() + "] is allowed", info.typesAllowed.contains(type.getName())); + } + } + + public void testBothTypesAllowedReturnsBothTypes() { + List types = Arrays.stream(ScriptType.values()).map(ScriptType::getName).collect(Collectors.toList()); + Settings.Builder settings = Settings.builder().putList("script.allowed_types", types); + ScriptService ss = getMockScriptService(settings.build()); + ScriptLanguagesInfo info = ss.getScriptLanguages(); + assertEquals(types.size(), info.typesAllowed.size()); + for(String type: types) { + assertTrue("[" + type + "] is allowed", info.typesAllowed.contains(type)); + } + } + + private ScriptService getMockScriptService(Settings settings) { + MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, + Collections.singletonMap("test_script", script -> 1), + Collections.emptyMap()); + Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); + + return new ScriptService(settings, engines, ScriptModule.CORE_CONTEXTS); + } + + + public interface MiscContext { + void execute(); + Object newInstance(); + } + + public void testOnlyScriptEngineContextsReturned() { + MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, + Collections.singletonMap("test_script", script -> 1), + Collections.emptyMap()); + Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); + + Map> mockContexts = scriptEngine.getSupportedContexts().stream().collect(Collectors.toMap( + c -> c.name, + Function.identity() + )); + String miscContext = "misc_context"; + assertFalse(mockContexts.containsKey(miscContext)); + + Map> mockAndMiscContexts = new HashMap<>(mockContexts); + mockAndMiscContexts.put(miscContext, new ScriptContext<>(miscContext, MiscContext.class)); + + ScriptService ss = new ScriptService(Settings.EMPTY, engines, mockAndMiscContexts); + ScriptLanguagesInfo info = ss.getScriptLanguages(); + + assertTrue(info.languageContexts.containsKey(MockScriptEngine.NAME)); + assertEquals(1, info.languageContexts.size()); + assertEquals(mockContexts.keySet(), info.languageContexts.get(MockScriptEngine.NAME)); + } + + public void testContextsAllowedSettingRespected() { + MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, + Collections.singletonMap("test_script", script -> 1), + Collections.emptyMap()); + Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); + Map> mockContexts = scriptEngine.getSupportedContexts().stream().collect(Collectors.toMap( + c -> c.name, + Function.identity() + )); + + List allContexts = new ArrayList<>(mockContexts.keySet()); + List allowed = allContexts.subList(0, allContexts.size()/2); + String miscContext = "misc_context"; + allowed.add(miscContext); + // check that allowing more than available doesn't pollute the returned contexts + Settings.Builder settings = Settings.builder().putList("script.allowed_contexts", allowed); + + Map> mockAndMiscContexts = new HashMap<>(mockContexts); + mockAndMiscContexts.put(miscContext, new ScriptContext<>(miscContext, MiscContext.class)); + + ScriptService ss = new ScriptService(settings.build(), engines, mockAndMiscContexts); + ScriptLanguagesInfo info = ss.getScriptLanguages(); + + assertTrue(info.languageContexts.containsKey(MockScriptEngine.NAME)); + assertEquals(1, info.languageContexts.size()); + assertEquals(new HashSet<>(allContexts.subList(0, allContexts.size()/2)), info.languageContexts.get(MockScriptEngine.NAME)); + } +} diff --git a/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java b/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java index 7bcc3e58cc2aa..e6bbcf5dd57d0 100644 --- a/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java +++ b/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java @@ -50,6 +50,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; import static org.elasticsearch.client.Requests.searchRequest; @@ -90,6 +91,11 @@ public ScoreScript newInstance(LeafReaderContext ctx) throws IOException { }; return context.factoryClazz.cast(factory); } + + @Override + public Set> getSupportedContexts() { + return Set.of(ScoreScript.CONTEXT); + } }; } } diff --git a/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java b/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java index f242eb8c18506..9495bc444265e 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java @@ -55,6 +55,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.ExecutionException; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS; @@ -1155,6 +1156,11 @@ public String execute() { }; return context.factoryClazz.cast(factory); } + + @Override + public Set> getSupportedContexts() { + return Set.of(TemplateScript.CONTEXT); + } } public void testPhraseSuggesterCollate() throws InterruptedException, ExecutionException, IOException { diff --git a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java index e16060a0c6786..e278fbad85aff 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java @@ -35,6 +35,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import static java.util.Collections.emptyMap; @@ -304,6 +305,35 @@ public double execute(Map params1, double[] values) { throw new IllegalArgumentException("mock script engine does not know how to handle context [" + context.name + "]"); } + @Override + public Set> getSupportedContexts() { + // TODO(stu): make part of `compile()` + return Set.of( + FieldScript.CONTEXT, + TermsSetQueryScript.CONTEXT, + NumberSortScript.CONTEXT, + StringSortScript.CONTEXT, + IngestScript.CONTEXT, + AggregationScript.CONTEXT, + IngestConditionalScript.CONTEXT, + UpdateScript.CONTEXT, + BucketAggregationScript.CONTEXT, + BucketAggregationSelectorScript.CONTEXT, + SignificantTermsHeuristicScoreScript.CONTEXT, + TemplateScript.CONTEXT, + FilterScript.CONTEXT, + SimilarityScript.CONTEXT, + SimilarityWeightScript.CONTEXT, + MovingFunctionScript.CONTEXT, + ScoreScript.CONTEXT, + ScriptedMetricAggContexts.InitScript.CONTEXT, + ScriptedMetricAggContexts.MapScript.CONTEXT, + ScriptedMetricAggContexts.CombineScript.CONTEXT, + ScriptedMetricAggContexts.ReduceScript.CONTEXT, + IntervalFilterScript.CONTEXT + ); + } + private Map createVars(Map params) { Map vars = new HashMap<>(); vars.put("params", params); From cf952e7e9e26a1cbe34e37c2f4926259a47177b2 Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Wed, 4 Dec 2019 07:07:09 +0100 Subject: [PATCH 059/686] [Transform] automatic deletion of old checkpoints (#49496) add automatic deletion of old checkpoints based on count and time --- .../transforms/TransformCheckpoint.java | 2 +- .../IndexBasedTransformConfigManager.java | 76 +++++++++++++--- .../persistence/TransformConfigManager.java | 12 +++ .../persistence/TransformInternalIndex.java | 76 ++++++++-------- .../transforms/TransformIndexer.java | 61 ++++++++++++- .../InMemoryTransformConfigManager.java | 10 +++ .../TransformConfigManagerTests.java | 90 +++++++++++++++++++ 7 files changed, 279 insertions(+), 48 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpoint.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpoint.java index 3e2dd844c362f..8431abc886d85 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpoint.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpoint.java @@ -50,7 +50,7 @@ public class TransformCheckpoint implements Writeable, ToXContentObject { // checkpoint of the indexes (sequence id's) public static final ParseField INDICES = new ParseField("indices"); - private static final String NAME = "data_frame_transform_checkpoint"; + public static final String NAME = "data_frame_transform_checkpoint"; private static final ConstructingObjectParser STRICT_PARSER = createParser(false); private static final ConstructingObjectParser LENIENT_PARSER = createParser(true); diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/IndexBasedTransformConfigManager.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/IndexBasedTransformConfigManager.java index 5b747e9de01e1..e3345aa6a2d83 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/IndexBasedTransformConfigManager.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/IndexBasedTransformConfigManager.java @@ -39,6 +39,7 @@ import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.reindex.AbstractBulkByScrollRequest; import org.elasticsearch.index.reindex.BulkByScrollResponse; import org.elasticsearch.index.reindex.DeleteByQueryAction; import org.elasticsearch.index.reindex.DeleteByQueryRequest; @@ -63,6 +64,7 @@ import java.util.List; import java.util.Set; +import static org.elasticsearch.index.mapper.MapperService.SINGLE_MAPPING_NAME; import static org.elasticsearch.xpack.core.ClientHelper.TRANSFORM_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; @@ -146,16 +148,18 @@ public void updateTransformConfiguration( @Override public void deleteOldTransformConfigurations(String transformId, ActionListener listener) { - DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest( + DeleteByQueryRequest deleteByQueryRequest = createDeleteByQueryRequest(); + deleteByQueryRequest.indices( TransformInternalIndexConstants.INDEX_NAME_PATTERN, TransformInternalIndexConstants.INDEX_NAME_PATTERN_DEPRECATED - ).setQuery( + ); + deleteByQueryRequest.setQuery( QueryBuilders.constantScoreQuery( QueryBuilders.boolQuery() .mustNot(QueryBuilders.termQuery("_index", TransformInternalIndexConstants.LATEST_INDEX_NAME)) .filter(QueryBuilders.termQuery("_id", TransformConfig.documentId(transformId))) ) - ).setIndicesOptions(IndicesOptions.lenientExpandOpen()); + ); executeAsyncWithOrigin( client, @@ -177,17 +181,18 @@ public void deleteOldTransformConfigurations(String transformId, ActionListener< @Override public void deleteOldTransformStoredDocuments(String transformId, ActionListener listener) { - DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest( + DeleteByQueryRequest deleteByQueryRequest = createDeleteByQueryRequest(); + deleteByQueryRequest.indices( TransformInternalIndexConstants.INDEX_NAME_PATTERN, TransformInternalIndexConstants.INDEX_NAME_PATTERN_DEPRECATED - ).setQuery( + ); + deleteByQueryRequest.setQuery( QueryBuilders.constantScoreQuery( QueryBuilders.boolQuery() .mustNot(QueryBuilders.termQuery("_index", TransformInternalIndexConstants.LATEST_INDEX_NAME)) .filter(QueryBuilders.termQuery("_id", TransformStoredDoc.documentId(transformId))) ) - ).setIndicesOptions(IndicesOptions.lenientExpandOpen()); - + ); executeAsyncWithOrigin( client, TRANSFORM_ORIGIN, @@ -206,6 +211,41 @@ public void deleteOldTransformStoredDocuments(String transformId, ActionListener ); } + @Override + public void deleteOldCheckpoints(String transformId, long deleteCheckpointsBelow, long deleteOlderThan, ActionListener listener) { + DeleteByQueryRequest deleteByQueryRequest = createDeleteByQueryRequest(); + deleteByQueryRequest.indices( + TransformInternalIndexConstants.INDEX_NAME_PATTERN, + TransformInternalIndexConstants.INDEX_NAME_PATTERN_DEPRECATED + ); + deleteByQueryRequest.setQuery( + QueryBuilders.boolQuery() + .filter(QueryBuilders.termQuery(TransformField.ID.getPreferredName(), transformId)) + .filter(QueryBuilders.termQuery(TransformField.INDEX_DOC_TYPE.getPreferredName(), TransformCheckpoint.NAME)) + .filter(QueryBuilders.rangeQuery(TransformCheckpoint.CHECKPOINT.getPreferredName()).lt(deleteCheckpointsBelow)) + .filter( + QueryBuilders.rangeQuery(TransformField.TIMESTAMP_MILLIS.getPreferredName()).lt(deleteOlderThan).format("epoch_millis") + ) + ); + logger.debug("Deleting old checkpoints using {}", deleteByQueryRequest.getSearchRequest()); + executeAsyncWithOrigin( + client, + TRANSFORM_ORIGIN, + DeleteByQueryAction.INSTANCE, + deleteByQueryRequest, + ActionListener.wrap(response -> { + if ((response.getBulkFailures().isEmpty() && response.getSearchFailures().isEmpty()) == false) { + Tuple statusAndReason = getStatusAndReason(response); + listener.onFailure( + new ElasticsearchStatusException(statusAndReason.v2().getMessage(), statusAndReason.v1(), statusAndReason.v2()) + ); + return; + } + listener.onResponse(response.getDeleted()); + }, listener::onFailure) + ); + } + private void putTransformConfiguration( TransformConfig transformConfig, DocWriteRequest.OpType optType, @@ -419,9 +459,7 @@ public void expandTransformIds( @Override public void deleteTransform(String transformId, ActionListener listener) { - DeleteByQueryRequest request = new DeleteByQueryRequest().setAbortOnVersionConflict(false); // since these documents are not - // updated, a conflict just means it was - // deleted previously + DeleteByQueryRequest request = createDeleteByQueryRequest(); request.indices(TransformInternalIndexConstants.INDEX_NAME_PATTERN, TransformInternalIndexConstants.INDEX_NAME_PATTERN_DEPRECATED); QueryBuilder query = QueryBuilders.termQuery(TransformField.ID.getPreferredName(), transformId); @@ -675,4 +713,22 @@ private static Tuple getStatusAndReason(final BulkByScrol } return new Tuple<>(status, reason); } + + /** + * Create DBQ request with good defaults + * + * @return new DeleteByQueryRequest with some defaults set + */ + private static DeleteByQueryRequest createDeleteByQueryRequest() { + + DeleteByQueryRequest deleteByQuery = new DeleteByQueryRequest(); + + deleteByQuery.setAbortOnVersionConflict(false) + .setSlices(AbstractBulkByScrollRequest.AUTO_SLICES) + .setIndicesOptions(IndicesOptions.lenientExpandOpen()); + + // disable scoring by using index order + deleteByQuery.getSearchRequest().source().sort(SINGLE_MAPPING_NAME); + return deleteByQuery; + } } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManager.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManager.java index 4d512166b1d16..61c639964e963 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManager.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManager.java @@ -72,6 +72,18 @@ void updateTransformConfiguration( */ void deleteOldTransformStoredDocuments(String transformId, ActionListener listener); + /** + * This deletes stored checkpoint documents for the given transformId, based on number and age. + * + * Both criteria MUST apply for the deletion to happen. + * + * @param transformId The transform ID referenced by the documents + * @param deleteCheckpointsBelow checkpoints lower than this to delete + * @param deleteOlderThan checkpoints older than this to delete + * @param listener listener to alert on completion, returning number of deleted checkpoints + */ + void deleteOldCheckpoints(String transformId, long deleteCheckpointsBelow, long deleteOlderThan, ActionListener listener); + /** * Get a stored checkpoint, requires the transform id as well as the checkpoint id * diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndex.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndex.java index d0b94bf6e3e52..2951ade8f47fb 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndex.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformInternalIndex.java @@ -28,6 +28,7 @@ import org.elasticsearch.xpack.core.transform.TransformField; import org.elasticsearch.xpack.core.transform.transforms.DestConfig; import org.elasticsearch.xpack.core.transform.transforms.SourceConfig; +import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpoint; import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerStats; import org.elasticsearch.xpack.core.transform.transforms.TransformProgress; import org.elasticsearch.xpack.core.transform.transforms.TransformState; @@ -54,8 +55,9 @@ public final class TransformInternalIndex { * progress::docs_processed, progress::docs_indexed, * stats::exponential_avg_checkpoint_duration_ms, stats::exponential_avg_documents_indexed, * stats::exponential_avg_documents_processed - * + * version 3 (7.5): rename to .transform-internal-xxx * version 4 (7.6): state::should_stop_at_checkpoint + * checkpoint::checkpoint */ // constants for mappings @@ -77,14 +79,16 @@ public final class TransformInternalIndex { public static IndexTemplateMetaData getIndexTemplateMetaData() throws IOException { IndexTemplateMetaData transformTemplate = IndexTemplateMetaData.builder(TransformInternalIndexConstants.LATEST_INDEX_VERSIONED_NAME) - .patterns(Collections.singletonList(TransformInternalIndexConstants.LATEST_INDEX_VERSIONED_NAME)) - .version(Version.CURRENT.id) - .settings(Settings.builder() - // the configurations are expected to be small - .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) - .put(IndexMetaData.SETTING_AUTO_EXPAND_REPLICAS, "0-1")) - .putMapping(MapperService.SINGLE_MAPPING_NAME, Strings.toString(mappings())) - .build(); + .patterns(Collections.singletonList(TransformInternalIndexConstants.LATEST_INDEX_VERSIONED_NAME)) + .version(Version.CURRENT.id) + .settings( + Settings.builder() + // the configurations are expected to be small + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_AUTO_EXPAND_REPLICAS, "0-1") + ) + .putMapping(MapperService.SINGLE_MAPPING_NAME, Strings.toString(mappings())) + .build(); return transformTemplate; } @@ -92,10 +96,12 @@ public static IndexTemplateMetaData getAuditIndexTemplateMetaData() throws IOExc IndexTemplateMetaData transformTemplate = IndexTemplateMetaData.builder(TransformInternalIndexConstants.AUDIT_INDEX) .patterns(Collections.singletonList(TransformInternalIndexConstants.AUDIT_INDEX_PREFIX + "*")) .version(Version.CURRENT.id) - .settings(Settings.builder() - // the audits are expected to be small - .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) - .put(IndexMetaData.SETTING_AUTO_EXPAND_REPLICAS, "0-1")) + .settings( + Settings.builder() + // the audits are expected to be small + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_AUTO_EXPAND_REPLICAS, "0-1") + ) .putMapping(MapperService.SINGLE_MAPPING_NAME, Strings.toString(auditMappings())) .putAlias(AliasMetaData.builder(TransformInternalIndexConstants.AUDIT_INDEX_READ_ALIAS)) .build(); @@ -107,26 +113,27 @@ private static XContentBuilder auditMappings() throws IOException { builder.startObject(SINGLE_MAPPING_NAME); addMetaInformation(builder); builder.field(DYNAMIC, "false"); - builder.startObject(PROPERTIES) - .startObject(TRANSFORM_ID) - .field(TYPE, KEYWORD) - .endObject() - .startObject(AbstractAuditMessage.LEVEL.getPreferredName()) - .field(TYPE, KEYWORD) - .endObject() - .startObject(AbstractAuditMessage.MESSAGE.getPreferredName()) - .field(TYPE, TEXT) - .startObject(FIELDS) - .startObject(RAW) - .field(TYPE, KEYWORD) - .endObject() - .endObject() + builder + .startObject(PROPERTIES) + .startObject(TRANSFORM_ID) + .field(TYPE, KEYWORD) + .endObject() + .startObject(AbstractAuditMessage.LEVEL.getPreferredName()) + .field(TYPE, KEYWORD) + .endObject() + .startObject(AbstractAuditMessage.MESSAGE.getPreferredName()) + .field(TYPE, TEXT) + .startObject(FIELDS) + .startObject(RAW) + .field(TYPE, KEYWORD) + .endObject() + .endObject() .endObject() .startObject(AbstractAuditMessage.TIMESTAMP.getPreferredName()) - .field(TYPE, DATE) + .field(TYPE, DATE) .endObject() .startObject(AbstractAuditMessage.NODE_NAME.getPreferredName()) - .field(TYPE, KEYWORD) + .field(TYPE, KEYWORD) .endObject() .endObject() .endObject() @@ -167,7 +174,6 @@ public static XContentBuilder mappings(XContentBuilder builder) throws IOExcepti return builder; } - private static XContentBuilder addTransformStoredDocMappings(XContentBuilder builder) throws IOException { return builder .startObject(TransformStoredDoc.STATE_FIELD.getPreferredName()) @@ -254,9 +260,6 @@ private static XContentBuilder addTransformStoredDocMappings(XContentBuilder bui .endObject() .endObject() .endObject(); - // This is obsolete and can be removed for future versions of the index, but is left here as a warning/reminder that - // we cannot declare this field differently in version 1 of the internal index as it would cause a mapping clash - // .startObject("checkpointing").field(ENABLED, false).endObject(); } public static XContentBuilder addTransformsConfigMappings(XContentBuilder builder) throws IOException { @@ -299,6 +302,9 @@ private static XContentBuilder addTransformCheckpointMappings(XContentBuilder bu .endObject() .startObject(TransformField.TIME_UPPER_BOUND_MILLIS.getPreferredName()) .field(TYPE, DATE) + .endObject() + .startObject(TransformCheckpoint.CHECKPOINT.getPreferredName()) + .field(TYPE, LONG) .endObject(); } @@ -310,9 +316,7 @@ private static XContentBuilder addTransformCheckpointMappings(XContentBuilder bu * @throws IOException On write error */ private static XContentBuilder addMetaInformation(XContentBuilder builder) throws IOException { - return builder.startObject("_meta") - .field("version", Version.CURRENT) - .endObject(); + return builder.startObject("_meta").field("version", Version.CURRENT).endObject(); } /** diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformIndexer.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformIndexer.java index 01c2b8c4329b1..0848fc21217e5 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformIndexer.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformIndexer.java @@ -79,8 +79,14 @@ private enum RunState { public static final int MINIMUM_PAGE_SIZE = 10; public static final String COMPOSITE_AGGREGATION_NAME = "_transform"; + private static final Logger logger = LogManager.getLogger(TransformIndexer.class); + // constant for checkpoint retention, static for now + private static final long NUMBER_OF_CHECKPOINTS_TO_KEEP = 10; + private static final long RETENTION_OF_CHECKPOINTS_MS = 864000000L; // 10 days + private static final long CHECKPOINT_CLEANUP_INTERVAL = 100L; // every 100 checkpoints + protected final TransformConfigManager transformsConfigManager; private final CheckpointProvider checkpointProvider; private final TransformProgressGatherer progressGatherer; @@ -111,6 +117,8 @@ private enum RunState { private volatile Map> changedBuckets; private volatile Map changedBucketsAfterKey; + private volatile long lastCheckpointCleanup = 0L; + public TransformIndexer( Executor executor, TransformConfigManager transformsConfigManager, @@ -375,7 +383,13 @@ protected void onFinish(ActionListener listener) { if (context.shouldStopAtCheckpoint()) { stop(); } - listener.onResponse(null); + + if (checkpoint - lastCheckpointCleanup > CHECKPOINT_CLEANUP_INTERVAL) { + // delete old checkpoints, on a failure we keep going + cleanupOldCheckpoints(listener); + } else { + listener.onResponse(null); + } } catch (Exception e) { listener.onFailure(e); } @@ -492,6 +506,44 @@ synchronized void handleFailure(Exception e) { } } + /** + * Cleanup old checkpoints + * + * @param listener listener to call after done + */ + private void cleanupOldCheckpoints(ActionListener listener) { + long now = getTime(); + long checkpointLowerBound = context.getCheckpoint() - NUMBER_OF_CHECKPOINTS_TO_KEEP; + long lowerBoundEpochMs = now - RETENTION_OF_CHECKPOINTS_MS; + + if (checkpointLowerBound > 0 && lowerBoundEpochMs > 0) { + transformsConfigManager.deleteOldCheckpoints( + transformConfig.getId(), + checkpointLowerBound, + lowerBoundEpochMs, + ActionListener.wrap(deletes -> { + logger.debug("[{}] deleted [{}] outdated checkpoints", getJobId(), deletes); + listener.onResponse(null); + lastCheckpointCleanup = context.getCheckpoint(); + }, e -> { + logger.warn( + new ParameterizedMessage("[{}] failed to cleanup old checkpoints, retrying after next checkpoint", getJobId()), + e + ); + auditor.warning( + getJobId(), + "Failed to cleanup old checkpoints, retrying after next checkpoint. Exception: " + e.getMessage() + ); + + listener.onResponse(null); + }) + ); + } else { + logger.debug("[{}] checked for outdated checkpoints", getJobId()); + listener.onResponse(null); + } + } + private void sourceHasChanged(ActionListener hasChangedListener) { checkpointProvider.sourceHasChanged(getLastCheckpoint(), ActionListener.wrap(hasChanged -> { logger.trace("[{}] change detected [{}].", getJobId(), hasChanged); @@ -788,6 +840,13 @@ protected void failIndexer(String failureMessage) { context.markAsFailed(failureMessage); } + /* + * Get the current time, abstracted for the purpose of testing + */ + long getTime() { + return System.currentTimeMillis(); + } + /** * Indicates if an audit message should be written when onFinish is called for the given checkpoint * We audit the first checkpoint, and then every 10 checkpoints until completedCheckpoint == 99 diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/InMemoryTransformConfigManager.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/InMemoryTransformConfigManager.java index 3b084c01a8a46..c52e8dea8fde2 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/InMemoryTransformConfigManager.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/InMemoryTransformConfigManager.java @@ -81,6 +81,16 @@ public void deleteOldTransformStoredDocuments(String transformId, ActionListener listener.onResponse(true); } + @Override + public void deleteOldCheckpoints(String transformId, long deleteCheckpointsBelow, long deleteOlderThan, ActionListener listener) { + List checkpointsById = checkpoints.get(transformId); + int sizeBeforeDelete = checkpointsById.size(); + if (checkpointsById != null) { + checkpointsById.removeIf(cp -> { return cp.getCheckpoint() < deleteCheckpointsBelow && cp.getTimestamp() < deleteOlderThan; }); + } + listener.onResponse(Long.valueOf(sizeBeforeDelete - checkpointsById.size())); + } + @Override public void getTransformCheckpoint(String transformId, long checkpoint, ActionListener resultListener) { List checkpointsById = checkpoints.get(transformId); diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManagerTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManagerTests.java index c3248a56afe92..818941bac5837 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManagerTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManagerTests.java @@ -425,4 +425,94 @@ public void testDeleteOldTransformStoredDocuments() throws Exception { is(true) ); } + + public void testDeleteOldCheckpoints() throws InterruptedException { + String transformId = randomAlphaOfLengthBetween(1, 10); + long timestamp = System.currentTimeMillis() - randomLongBetween(20000, 40000); + + // create some other docs to check they are not getting accidentally deleted + TransformStoredDoc storedDocs = TransformStoredDocTests.randomTransformStoredDoc(transformId); + SeqNoPrimaryTermAndIndex firstIndex = new SeqNoPrimaryTermAndIndex(0, 1, TransformInternalIndexConstants.LATEST_INDEX_NAME); + assertAsync(listener -> transformConfigManager.putOrUpdateTransformStoredDoc(storedDocs, null, listener), firstIndex, null, null); + + TransformConfig transformConfig = TransformConfigTests.randomTransformConfig(transformId); + assertAsync(listener -> transformConfigManager.putTransformConfiguration(transformConfig, listener), true, null, null); + + // create 100 checkpoints + for (int i = 1; i <= 100; i++) { + TransformCheckpoint checkpoint = new TransformCheckpoint( + transformId, + timestamp + i * 200, + i, + Collections.emptyMap(), + timestamp - 100 + i * 200 + ); + assertAsync(listener -> transformConfigManager.putTransformCheckpoint(checkpoint, listener), true, null, null); + } + + // read a random checkpoint + int randomCheckpoint = randomIntBetween(1, 100); + TransformCheckpoint checkpointExpected = new TransformCheckpoint( + transformId, + timestamp + randomCheckpoint * 200, + randomCheckpoint, + Collections.emptyMap(), + timestamp - 100 + randomCheckpoint * 200 + ); + + assertAsync( + listener -> transformConfigManager.getTransformCheckpoint(transformId, randomCheckpoint, listener), + checkpointExpected, + null, + null + ); + + // test delete based on checkpoint number (time would allow more) + assertAsync( + listener -> transformConfigManager.deleteOldCheckpoints(transformId, 11L, timestamp + 1 + 20L * 200, listener), + 10L, + null, + null + ); + + // test delete based on time (checkpoint number would allow more) + assertAsync( + listener -> transformConfigManager.deleteOldCheckpoints(transformId, 30L, timestamp + 1 + 20L * 200, listener), + 10L, + null, + null + ); + + // zero delete + assertAsync( + listener -> transformConfigManager.deleteOldCheckpoints(transformId, 30L, timestamp + 1 + 20L * 200, listener), + 0L, + null, + null + ); + + // delete the rest + assertAsync( + listener -> transformConfigManager.deleteOldCheckpoints(transformId, 101L, timestamp + 1 + 100L * 200, listener), + 80L, + null, + null + ); + + // test that the other docs are still there + assertAsync( + listener -> transformConfigManager.getTransformStoredDoc(transformId, listener), + Tuple.tuple(storedDocs, firstIndex), + null, + null + ); + + assertAsync( + listener -> transformConfigManager.getTransformConfiguration(transformConfig.getId(), listener), + transformConfig, + null, + null + ); + + } } From 03bf4fc09f77ba065d576ae4d6f63a9c9a816608 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Wed, 4 Dec 2019 08:47:40 +0000 Subject: [PATCH 060/686] Fixes a bug in interval filter serialization (#49793) There is a possible NPE in IntervalFilter xcontent serialization when scripts are used, and `equals` and `hashCode` are also incorrectly implemented for script filters. This commit fixes both. --- .../index/query/IntervalsSourceProvider.java | 14 +++++++++----- .../index/query/IntervalQueryBuilderTests.java | 7 ++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java index 81cc1524549a8..97093f0c92fc9 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java @@ -749,12 +749,13 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; IntervalFilter that = (IntervalFilter) o; return Objects.equals(type, that.type) && + Objects.equals(script, that.script) && Objects.equals(filter, that.filter); } @Override public int hashCode() { - return Objects.hash(type, filter); + return Objects.hash(type, filter, script); } @Override @@ -773,10 +774,13 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(type); - builder.startObject(); - filter.toXContent(builder, params); - builder.endObject(); + if (filter != null) { + builder.startObject(type); + filter.toXContent(builder, params); + builder.endObject(); + } else { + builder.field(Script.SCRIPT_PARSE_FIELD.getPreferredName(), script); + } builder.endObject(); return builder; } diff --git a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java index da1da5ce54b69..c5cfca0b7fe9f 100644 --- a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java @@ -36,6 +36,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; import org.elasticsearch.test.AbstractQueryTestCase; import java.io.IOException; @@ -114,7 +115,11 @@ private IntervalsSourceProvider createRandomSource(int depth) { private IntervalsSourceProvider.IntervalFilter createRandomFilter(int depth) { if (depth < 3 && randomInt(20) > 18) { - return new IntervalsSourceProvider.IntervalFilter(createRandomSource(depth + 1), randomFrom(filters)); + if (randomBoolean()) { + return new IntervalsSourceProvider.IntervalFilter(createRandomSource(depth + 1), randomFrom(filters)); + } + return new IntervalsSourceProvider.IntervalFilter( + new Script(ScriptType.INLINE, "mockscript", "1", Collections.emptyMap())); } return null; } From ad604760da73a7f256653e607175149e38c15ab5 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Wed, 4 Dec 2019 10:58:44 +0100 Subject: [PATCH 061/686] Add reusable HistogramValue object (#49799) Adds a reusable implementation of HistogramValue so we do not create an object per document. --- .../mapper/HistogramFieldMapper.java | 107 ++++++++++-------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java index 6ef920bd33fa3..b22f6eb0573df 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java @@ -18,11 +18,11 @@ import org.apache.lucene.search.DocValuesFieldExistsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.SortField; +import org.apache.lucene.store.ByteArrayDataInput; +import org.apache.lucene.store.ByteBuffersDataOutput; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.Explicit; import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.io.stream.ByteBufferStreamInput; -import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; @@ -49,7 +49,6 @@ import org.elasticsearch.search.MultiValueMode; import java.io.IOException; -import java.nio.ByteBuffer; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -202,7 +201,9 @@ public AtomicHistogramFieldData load(LeafReaderContext context) { public HistogramValues getHistogramValues() throws IOException { try { final BinaryDocValues values = DocValues.getBinary(context.reader(), fieldName); + final InternalHistogramValue value = new InternalHistogramValue(); return new HistogramValues() { + @Override public boolean advanceExact(int doc) throws IOException { return values.advanceExact(doc); @@ -211,7 +212,8 @@ public boolean advanceExact(int doc) throws IOException { @Override public HistogramValue histogram() throws IOException { try { - return getHistogramValue(values.binaryValue()); + value.reset(values.binaryValue()); + return value; } catch (IOException e) { throw new IOException("Cannot load doc value", e); } @@ -220,7 +222,6 @@ public HistogramValue histogram() throws IOException { } catch (IOException e) { throw new IOException("Cannot load doc values", e); } - } @Override @@ -259,44 +260,6 @@ public SortField sortField(Object missingValue, MultiValueMode sortMode, } }; } - - private HistogramValue getHistogramValue(final BytesRef bytesRef) throws IOException { - final ByteBufferStreamInput streamInput = new ByteBufferStreamInput( - ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length)); - return new HistogramValue() { - double value; - int count; - boolean isExhausted; - - @Override - public boolean next() throws IOException { - if (streamInput.available() > 0) { - count = streamInput.readVInt(); - value = streamInput.readDouble(); - return true; - } - isExhausted = true; - return false; - } - - @Override - public double value() { - if (isExhausted) { - throw new IllegalArgumentException("histogram already exhausted"); - } - return value; - } - - @Override - public int count() { - if (isExhausted) { - throw new IllegalArgumentException("histogram already exhausted"); - } - return count; - } - }; - } - }; } @@ -395,7 +358,7 @@ public void parse(ParseContext context) throws IOException { "[" + COUNTS_FIELD.getPreferredName() +"] but got [" + values.size() + " != " + counts.size() +"]"); } if (fieldType().hasDocValues()) { - BytesStreamOutput streamOutput = new BytesStreamOutput(); + ByteBuffersDataOutput dataOutput = new ByteBuffersDataOutput(); for (int i = 0; i < values.size(); i++) { int count = counts.get(i); if (count < 0) { @@ -403,13 +366,12 @@ public void parse(ParseContext context) throws IOException { + name() + "], ["+ COUNTS_FIELD + "] elements must be >= 0 but got " + counts.get(i)); } else if (count > 0) { // we do not add elements with count == 0 - streamOutput.writeVInt(count); - streamOutput.writeDouble(values.get(i)); + dataOutput.writeVInt(count); + dataOutput.writeLong(Double.doubleToRawLongBits(values.get(i))); } } - - Field field = new BinaryDocValuesField(simpleName(), streamOutput.bytes().toBytesRef()); - streamOutput.close(); + BytesRef docValue = new BytesRef(dataOutput.toArrayCopy(), 0, Math.toIntExact(dataOutput.size())); + Field field = new BinaryDocValuesField(simpleName(), docValue); if (context.doc().getByKey(fieldType().name()) != null) { throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] doesn't not support indexing multiple values for the same field in the same document"); @@ -439,4 +401,51 @@ protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, builder.field(Names.IGNORE_MALFORMED, ignoreMalformed.value()); } } + + /** re-usable {@link HistogramValue} implementation */ + private static class InternalHistogramValue extends HistogramValue { + double value; + int count; + boolean isExhausted; + ByteArrayDataInput dataInput; + + InternalHistogramValue() { + dataInput = new ByteArrayDataInput(); + } + + /** reset the value for the histogram */ + void reset(BytesRef bytesRef) { + dataInput.reset(bytesRef.bytes, bytesRef.offset, bytesRef.length); + isExhausted = false; + value = 0; + count = 0; + } + + @Override + public boolean next() { + if (dataInput.eof() == false) { + count = dataInput.readVInt(); + value = Double.longBitsToDouble(dataInput.readLong()); + return true; + } + isExhausted = true; + return false; + } + + @Override + public double value() { + if (isExhausted) { + throw new IllegalArgumentException("histogram already exhausted"); + } + return value; + } + + @Override + public int count() { + if (isExhausted) { + throw new IllegalArgumentException("histogram already exhausted"); + } + return count; + } + } } From 9912f982ecaaec5d5ee3920e0064328fe7d7d573 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Wed, 4 Dec 2019 11:13:32 +0100 Subject: [PATCH 062/686] Fix invalid break iterator highlighting on keyword field (#49566) By default the unified highlighter splits the input into passages using a sentence break iterator. However we don't check if the field is tokenized or not so `keyword` field also applies the break iterator even though they can only match on the entire content. This means that by default we'll split the content of a `keyword` field on sentence break if the requested number of fragments is set to a value different than 0 (default to 5). This commit changes this behavior to ignore the break iterator on non-tokenized fields (keyword) in order to always highlight the entire values. The number of requested fragments control the number of matched values are returned but the boundary_scanner_type is now ignored. Note that this is the behavior in 6x but some refactoring of the Lucene's highlighter exposed this bug in Elasticsearch 7x. --- .../highlight/UnifiedHighlighter.java | 8 +++-- .../highlight/HighlighterSearchIT.java | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/UnifiedHighlighter.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/UnifiedHighlighter.java index e02e628418569..81f700920f827 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/UnifiedHighlighter.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/UnifiedHighlighter.java @@ -67,7 +67,7 @@ public HighlightField highlight(HighlighterContext highlighterContext) { final int maxAnalyzedOffset = context.getIndexSettings().getHighlightMaxAnalyzedOffset(); List snippets = new ArrayList<>(); - int numberOfFragments; + int numberOfFragments = field.fieldOptions().numberOfFragments(); try { final Analyzer analyzer = getAnalyzer(context.getMapperService().documentMapper(), hitContext); @@ -89,14 +89,16 @@ public HighlightField highlight(HighlighterContext highlighterContext) { "This maximum can be set by changing the [" + IndexSettings.MAX_ANALYZED_OFFSET_SETTING.getKey() + "] index level setting. " + "For large texts, indexing with offsets or term vectors is recommended!"); } - if (field.fieldOptions().numberOfFragments() == 0) { + if (numberOfFragments == 0 + // non-tokenized fields should not use any break iterator (ignore boundaryScannerType) + || fieldType.tokenized() == false) { // we use a control char to separate values, which is the only char that the custom break iterator // breaks the text on, so we don't lose the distinction between the different values of a field and we // get back a snippet per value CustomSeparatorBreakIterator breakIterator = new CustomSeparatorBreakIterator(MULTIVAL_SEP_CHAR); highlighter = new CustomUnifiedHighlighter(searcher, analyzer, offsetSource, passageFormatter, field.fieldOptions().boundaryScannerLocale(), breakIterator, fieldValue, field.fieldOptions().noMatchSize()); - numberOfFragments = fieldValues.size(); // we are highlighting the whole content, one snippet per value + numberOfFragments = numberOfFragments == 0 ? fieldValues.size() : numberOfFragments; } else { //using paragraph separator we make sure that each field value holds a discrete passage for highlighting BreakIterator bi = getBreakIterator(field); diff --git a/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java b/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java index 40ac220ec4853..539f9d1977a6c 100644 --- a/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java @@ -120,6 +120,35 @@ protected Collection> nodePlugins() { return Arrays.asList(InternalSettingsPlugin.class, MockKeywordPlugin.class, MockAnalysisPlugin.class); } + public void testHighlightingWithKeywordIgnoreBoundaryScanner() throws IOException { + XContentBuilder mappings = jsonBuilder(); + mappings.startObject(); + mappings.startObject("type") + .startObject("properties") + .startObject("tags") + .field("type", "keyword") + .endObject() + .endObject().endObject(); + mappings.endObject(); + assertAcked(prepareCreate("test") + .addMapping("type", mappings)); + client().prepareIndex("test").setId("1") + .setSource(jsonBuilder().startObject().array("tags", "foo bar", "foo bar", "foo bar", "foo baz").endObject()) + .get(); + client().prepareIndex("test").setId("2") + .setSource(jsonBuilder().startObject().array("tags", "foo baz", "foo baz", "foo baz", "foo bar").endObject()) + .get(); + refresh(); + + for (BoundaryScannerType scanner : BoundaryScannerType.values()) { + SearchResponse search = client().prepareSearch().setQuery(matchQuery("tags", "foo bar")) + .highlighter(new HighlightBuilder().field(new Field("tags")).numOfFragments(2).boundaryScannerType(scanner)).get(); + assertHighlight(search, 0, "tags", 0, 2, equalTo("foo bar")); + assertHighlight(search, 0, "tags", 1, 2, equalTo("foo bar")); + assertHighlight(search, 1, "tags", 0, 1, equalTo("foo bar")); + } + } + public void testHighlightingWithStoredKeyword() throws IOException { XContentBuilder mappings = jsonBuilder(); mappings.startObject(); From b3a6f47e2734fdde91c1ede0e1bf1ead68186c80 Mon Sep 17 00:00:00 2001 From: jimczi Date: Wed, 4 Dec 2019 12:21:02 +0100 Subject: [PATCH 063/686] #49566 Fix non-deterministic sort order in testHighlightingWithKeywordIgnoreBoundaryScanner --- .../highlight/HighlighterSearchIT.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java b/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java index 539f9d1977a6c..68077456a6c5f 100644 --- a/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java @@ -57,6 +57,7 @@ import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder.BoundaryScannerType; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder.Field; +import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.InternalSettingsPlugin; @@ -128,20 +129,33 @@ public void testHighlightingWithKeywordIgnoreBoundaryScanner() throws IOExceptio .startObject("tags") .field("type", "keyword") .endObject() + .startObject("sort") + .field("type", "long") + .endObject() .endObject().endObject(); mappings.endObject(); assertAcked(prepareCreate("test") .addMapping("type", mappings)); client().prepareIndex("test").setId("1") - .setSource(jsonBuilder().startObject().array("tags", "foo bar", "foo bar", "foo bar", "foo baz").endObject()) + .setSource(jsonBuilder() + .startObject() + .array("tags", "foo bar", "foo bar", "foo bar", "foo baz") + .field("sort", 1) + .endObject()) .get(); client().prepareIndex("test").setId("2") - .setSource(jsonBuilder().startObject().array("tags", "foo baz", "foo baz", "foo baz", "foo bar").endObject()) + .setSource(jsonBuilder() + .startObject() + .array("tags", "foo baz", "foo baz", "foo baz", "foo bar") + .field("sort", 2) + .endObject()) .get(); refresh(); for (BoundaryScannerType scanner : BoundaryScannerType.values()) { - SearchResponse search = client().prepareSearch().setQuery(matchQuery("tags", "foo bar")) + SearchResponse search = client().prepareSearch() + .addSort(SortBuilders.fieldSort("sort")) + .setQuery(matchQuery("tags", "foo bar")) .highlighter(new HighlightBuilder().field(new Field("tags")).numOfFragments(2).boundaryScannerType(scanner)).get(); assertHighlight(search, 0, "tags", 0, 2, equalTo("foo bar")); assertHighlight(search, 0, "tags", 1, 2, equalTo("foo bar")); From b3e3e189e671ded771ebdd5a237dd07814654331 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Wed, 4 Dec 2019 11:32:32 +0000 Subject: [PATCH 064/686] Improve coverage of equals/hashCode tests for IntervalQueryBuilder (#49820) By default, AbstractQueryTestCase only changes name and boost in its mutateInstance method, used when checking equals and hashcode implementations. This commit adds a mutateInstance method to InveralQueryBuilderTests that will check hashcode and equality when the field or intervals source are changed. --- .../index/query/IntervalQueryBuilder.java | 8 ++++++++ .../index/query/IntervalQueryBuilderTests.java | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/IntervalQueryBuilder.java index 4813adfc4d3db..7e94b5da69143 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IntervalQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalQueryBuilder.java @@ -55,6 +55,14 @@ public IntervalQueryBuilder(StreamInput in) throws IOException { this.sourceProvider = in.readNamedWriteable(IntervalsSourceProvider.class); } + public String getField() { + return field; + } + + public IntervalsSourceProvider getSourceProvider() { + return sourceProvider; + } + @Override protected void doWriteTo(StreamOutput out) throws IOException { out.writeString(field); diff --git a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java index c5cfca0b7fe9f..5fd91848a7064 100644 --- a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java @@ -143,6 +143,17 @@ protected void doAssertLuceneQuery(IntervalQueryBuilder queryBuilder, Query quer assertThat(query, instanceOf(IntervalQuery.class)); } + @Override + public IntervalQueryBuilder mutateInstance(IntervalQueryBuilder instance) throws IOException { + if (randomBoolean()) { + return super.mutateInstance(instance); // just change name/boost + } + if (randomBoolean()) { + return new IntervalQueryBuilder(STRING_FIELD_NAME_2, instance.getSourceProvider()); + } + return new IntervalQueryBuilder(STRING_FIELD_NAME, createRandomSource(0)); + } + public void testMatchInterval() throws IOException { String json = "{ \"intervals\" : " + From 2a5e48030c140f367af9300b3f7beb58f1172a8b Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 4 Dec 2019 13:01:06 +0100 Subject: [PATCH 065/686] Use Cluster State to Track Repository Generation (#49729) Step on the road to #49060. This commit adds the logic to keep track of a repository's generation across repository operations. See changes to package level Javadoc for the concrete changes in the distributed state machine. It updates the write side of new repository generations to be fully consistent via the cluster state. With this change, no `index-N` will be overwritten for the same repository ever. So eventual consistency issues around conflicting updates to the same `index-N` are not a possibility any longer. With this change the read side will still use listing of repository contents instead of relying solely on the cluster state contents. The logic for that will be introduced in #49060. This retains the ability to externally delete the contents of a repository and continue using it afterwards for the time being. In #49060 the use of listing to determine the repository generation will be removed in all cases (except for full-cluster restart) as the last step in this effort. --- .../get/GetRepositoriesResponse.java | 3 +- .../metadata/RepositoriesMetaData.java | 78 +++++++- .../cluster/metadata/RepositoryMetaData.java | 85 ++++++++- .../repositories/RepositoriesService.java | 7 +- .../repositories/RepositoryData.java | 6 + .../blobstore/BlobStoreRepository.java | 171 ++++++++++++++---- .../repositories/blobstore/package-info.java | 46 ++++- .../repositories/fs/FsRepository.java | 2 +- .../BlobStoreRepositoryRestoreTests.java | 2 +- .../blobstore/BlobStoreRepositoryTests.java | 19 +- .../DedicatedClusterSnapshotRestoreIT.java | 7 +- ...epositoriesMetaDataSerializationTests.java | 5 +- .../SharedClusterSnapshotRestoreIT.java | 5 +- .../MockEventuallyConsistentRepository.java | 10 +- ...ckEventuallyConsistentRepositoryTests.java | 9 +- .../blobstore/BlobStoreTestUtil.java | 25 ++- .../snapshots/mockstore/MockRepository.java | 8 + .../SourceOnlySnapshotShardTests.java | 3 +- 18 files changed, 412 insertions(+), 79 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/GetRepositoriesResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/GetRepositoriesResponse.java index 69ade6d8fe0b0..7512e5a4f3b3e 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/GetRepositoriesResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/GetRepositoriesResponse.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.util.List; +import java.util.Map; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; @@ -66,7 +67,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - repositories.toXContent(builder, params); + repositories.toXContent(builder, new DelegatingMapParams(Map.of(RepositoriesMetaData.HIDE_GENERATIONS_PARAM, "true"), params)); builder.endObject(); return builder; } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetaData.java index 4f182b6ca381e..a28850a9b47ce 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetaData.java @@ -24,12 +24,15 @@ import org.elasticsearch.cluster.AbstractNamedDiffable; import org.elasticsearch.cluster.NamedDiff; import org.elasticsearch.cluster.metadata.MetaData.Custom; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.repositories.RepositoryData; import java.io.IOException; import java.util.ArrayList; @@ -44,6 +47,12 @@ public class RepositoriesMetaData extends AbstractNamedDiffable implemen public static final String TYPE = "repositories"; + /** + * Serialization parameter used to hide the {@link RepositoryMetaData#generation()} and {@link RepositoryMetaData#pendingGeneration()} + * in {@link org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesResponse}. + */ + public static final String HIDE_GENERATIONS_PARAM = "hide_generations"; + private final List repositories; /** @@ -55,6 +64,30 @@ public RepositoriesMetaData(List repositories) { this.repositories = Collections.unmodifiableList(repositories); } + /** + * Creates a new instance that has the given repository moved to the given {@code safeGeneration} and {@code pendingGeneration}. + * + * @param repoName repository name + * @param safeGeneration new safe generation + * @param pendingGeneration new pending generation + * @return new instance with updated generations + */ + public RepositoriesMetaData withUpdatedGeneration(String repoName, long safeGeneration, long pendingGeneration) { + int indexOfRepo = -1; + for (int i = 0; i < repositories.size(); i++) { + if (repositories.get(i).name().equals(repoName)) { + indexOfRepo = i; + break; + } + } + if (indexOfRepo < 0) { + throw new IllegalArgumentException("Unknown repository [" + repoName + "]"); + } + final List updatedRepos = new ArrayList<>(repositories); + updatedRepos.set(indexOfRepo, new RepositoryMetaData(repositories.get(indexOfRepo), safeGeneration, pendingGeneration)); + return new RepositoriesMetaData(updatedRepos); + } + /** * Returns list of currently registered repositories * @@ -87,7 +120,29 @@ public boolean equals(Object o) { RepositoriesMetaData that = (RepositoriesMetaData) o; return repositories.equals(that.repositories); + } + /** + * Checks if this instance and the given instance share the same repositories by checking that this instances' repositories and the + * repositories in {@code other} are equal or only differ in their values of {@link RepositoryMetaData#generation()} and + * {@link RepositoryMetaData#pendingGeneration()}. + * + * @param other other repositories metadata + * @return {@code true} iff both instances contain the same repositories apart from differences in generations + */ + public boolean equalsIgnoreGenerations(@Nullable RepositoriesMetaData other) { + if (other == null) { + return false; + } + if (other.repositories.size() != repositories.size()) { + return false; + } + for (int i = 0; i < repositories.size(); i++) { + if (repositories.get(i).equalsIgnoreGenerations(other.repositories.get(i)) == false) { + return false; + } + } + return true; } @Override @@ -142,6 +197,8 @@ public static RepositoriesMetaData fromXContent(XContentParser parser) throws IO } String type = null; Settings settings = Settings.EMPTY; + long generation = RepositoryData.UNKNOWN_REPO_GEN; + long pendingGeneration = RepositoryData.EMPTY_REPO_GEN; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { String currentFieldName = parser.currentName(); @@ -155,6 +212,16 @@ public static RepositoriesMetaData fromXContent(XContentParser parser) throws IO throw new ElasticsearchParseException("failed to parse repository [{}], incompatible params", name); } settings = Settings.fromXContent(parser); + } else if ("generation".equals(currentFieldName)) { + if (parser.nextToken() != XContentParser.Token.VALUE_NUMBER) { + throw new ElasticsearchParseException("failed to parse repository [{}], unknown type", name); + } + generation = parser.longValue(); + } else if ("pending_generation".equals(currentFieldName)) { + if (parser.nextToken() != XContentParser.Token.VALUE_NUMBER) { + throw new ElasticsearchParseException("failed to parse repository [{}], unknown type", name); + } + pendingGeneration = parser.longValue(); } else { throw new ElasticsearchParseException("failed to parse repository [{}], unknown field [{}]", name, currentFieldName); @@ -166,7 +233,7 @@ public static RepositoriesMetaData fromXContent(XContentParser parser) throws IO if (type == null) { throw new ElasticsearchParseException("failed to parse repository [{}], missing repository type", name); } - repository.add(new RepositoryMetaData(name, type, settings)); + repository.add(new RepositoryMetaData(name, type, settings, generation, pendingGeneration)); } else { throw new ElasticsearchParseException("failed to parse repositories"); } @@ -204,6 +271,15 @@ public static void toXContent(RepositoryMetaData repository, XContentBuilder bui repository.settings().toXContent(builder, params); builder.endObject(); + if (params.paramAsBoolean(HIDE_GENERATIONS_PARAM, false) == false) { + builder.field("generation", repository.generation()); + builder.field("pending_generation", repository.pendingGeneration()); + } builder.endObject(); } + + @Override + public String toString() { + return Strings.toString(this); + } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java index 847db915b8bce..c210c32a9a529 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java @@ -18,20 +18,36 @@ */ package org.elasticsearch.cluster.metadata; +import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.repositories.RepositoryData; import java.io.IOException; +import java.util.Objects; /** * Metadata about registered repository */ public class RepositoryMetaData { + + public static final Version REPO_GEN_IN_CS_VERSION = Version.V_8_0_0; + private final String name; private final String type; private final Settings settings; + /** + * Safe repository generation. + */ + private final long generation; + + /** + * Pending repository generation. + */ + private final long pendingGeneration; + /** * Constructs new repository metadata * @@ -40,9 +56,21 @@ public class RepositoryMetaData { * @param settings repository settings */ public RepositoryMetaData(String name, String type, Settings settings) { + this(name, type, settings, RepositoryData.UNKNOWN_REPO_GEN, RepositoryData.EMPTY_REPO_GEN); + } + + public RepositoryMetaData(RepositoryMetaData metaData, long generation, long pendingGeneration) { + this(metaData.name, metaData.type, metaData.settings, generation, pendingGeneration); + } + + public RepositoryMetaData(String name, String type, Settings settings, long generation, long pendingGeneration) { this.name = name; this.type = type; this.settings = settings; + this.generation = generation; + this.pendingGeneration = pendingGeneration; + assert generation <= pendingGeneration : + "Pending generation [" + pendingGeneration + "] must be greater or equal to generation [" + generation + "]"; } /** @@ -72,11 +100,41 @@ public Settings settings() { return this.settings; } + /** + * Returns the safe repository generation. {@link RepositoryData} for this generation is assumed to exist in the repository. + * All operations on the repository must be based on the {@link RepositoryData} at this generation. + * See package level documentation for the blob store based repositories {@link org.elasticsearch.repositories.blobstore} for details + * on how this value is used during snapshots. + * @return safe repository generation + */ + public long generation() { + return generation; + } + + /** + * Returns the pending repository generation. {@link RepositoryData} for this generation and all generations down to the safe + * generation {@link #generation} may exist in the repository and should not be reused for writing new {@link RepositoryData} to the + * repository. + * See package level documentation for the blob store based repositories {@link org.elasticsearch.repositories.blobstore} for details + * on how this value is used during snapshots. + * + * @return highest pending repository generation + */ + public long pendingGeneration() { + return pendingGeneration; + } public RepositoryMetaData(StreamInput in) throws IOException { name = in.readString(); type = in.readString(); settings = Settings.readSettingsFromStream(in); + if (in.getVersion().onOrAfter(REPO_GEN_IN_CS_VERSION)) { + generation = in.readLong(); + pendingGeneration = in.readLong(); + } else { + generation = RepositoryData.UNKNOWN_REPO_GEN; + pendingGeneration = RepositoryData.EMPTY_REPO_GEN; + } } /** @@ -88,6 +146,20 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(name); out.writeString(type); Settings.writeSettingsToStream(settings, out); + if (out.getVersion().onOrAfter(REPO_GEN_IN_CS_VERSION)) { + out.writeLong(generation); + out.writeLong(pendingGeneration); + } + } + + /** + * Checks if this instance is equal to the other instance in all fields other than {@link #generation} and {@link #pendingGeneration}. + * + * @param other other repository metadata + * @return {@code true} if both instances equal in all fields but the generation fields + */ + public boolean equalsIgnoreGenerations(RepositoryMetaData other) { + return name.equals(other.name) && type.equals(other.type()) && settings.equals(other.settings()); } @Override @@ -99,15 +171,18 @@ public boolean equals(Object o) { if (!name.equals(that.name)) return false; if (!type.equals(that.type)) return false; + if (generation != that.generation) return false; + if (pendingGeneration != that.pendingGeneration) return false; return settings.equals(that.settings); - } @Override public int hashCode() { - int result = name.hashCode(); - result = 31 * result + type.hashCode(); - result = 31 * result + settings.hashCode(); - return result; + return Objects.hash(name, type, settings, generation, pendingGeneration); + } + + @Override + public String toString() { + return "RepositoryMetaData{" + name + "}{" + type + "}{" + settings + "}{" + generation + "}{" + pendingGeneration + "}"; } } diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 03b283c4aafe5..c2796d27aa956 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -150,7 +150,7 @@ public ClusterState execute(ClusterState currentState) { for (RepositoryMetaData repositoryMetaData : repositories.repositories()) { if (repositoryMetaData.name().equals(newRepositoryMetaData.name())) { - if (newRepositoryMetaData.equals(repositoryMetaData)) { + if (newRepositoryMetaData.equalsIgnoreGenerations(repositoryMetaData)) { // Previous version is the same as this one no update is needed. return currentState; } @@ -292,7 +292,10 @@ public void applyClusterState(ClusterChangedEvent event) { RepositoriesMetaData newMetaData = state.getMetaData().custom(RepositoriesMetaData.TYPE); // Check if repositories got changed - if ((oldMetaData == null && newMetaData == null) || (oldMetaData != null && oldMetaData.equals(newMetaData))) { + if ((oldMetaData == null && newMetaData == null) || (oldMetaData != null && oldMetaData.equalsIgnoreGenerations(newMetaData))) { + for (Repository repo : repositories.values()) { + repo.updateState(state); + } return; } diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java b/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java index 20dcdc2371805..357268fa051e0 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java @@ -52,6 +52,12 @@ public final class RepositoryData { * The generation value indicating the repository has no index generational files. */ public static final long EMPTY_REPO_GEN = -1L; + + /** + * The generation value indicating that the repository generation is unknown. + */ + public static final long UNKNOWN_REPO_GEN = -2L; + /** * An instance initialized for an empty repository. */ diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 1e3076b258104..ba670c2d6f01d 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -36,11 +36,13 @@ import org.elasticsearch.action.StepListener; import org.elasticsearch.action.support.GroupedActionListener; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.RepositoryCleanupInProgress; import org.elasticsearch.cluster.SnapshotDeletionsInProgress; import org.elasticsearch.cluster.SnapshotsInProgress; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.metadata.RepositoriesMetaData; import org.elasticsearch.cluster.metadata.RepositoryMetaData; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.service.ClusterService; @@ -124,6 +126,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.LongStream; import java.util.stream.Stream; import static org.elasticsearch.index.snapshots.blobstore.BlobStoreIndexShardSnapshot.FileInfo.canonicalName; @@ -140,7 +143,7 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent implements Repository { private static final Logger logger = LogManager.getLogger(BlobStoreRepository.class); - protected final RepositoryMetaData metadata; + protected volatile RepositoryMetaData metadata; protected final ThreadPool threadPool; @@ -211,6 +214,8 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp private final BlobPath basePath; + private final ClusterService clusterService; + /** * Constructs new BlobStoreRepository * @param metadata The metadata for this repository including name and settings @@ -223,6 +228,7 @@ protected BlobStoreRepository( final BlobPath basePath) { this.metadata = metadata; this.threadPool = clusterService.getClusterApplierService().threadPool(); + this.clusterService = clusterService; this.compress = COMPRESS_SETTING.get(metadata.settings()); snapshotRateLimiter = getRateLimiter(metadata.settings(), "max_snapshot_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB)); restoreRateLimiter = getRateLimiter(metadata.settings(), "max_restore_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB)); @@ -293,7 +299,8 @@ public void updateState(ClusterState state) { bestGenerationFromCS = bestGeneration(cleanupInProgress.entries()); } - final long finalBestGen = bestGenerationFromCS; + metadata = getRepoMetaData(state); + final long finalBestGen = Math.max(bestGenerationFromCS, metadata.generation()); latestKnownRepoGen.updateAndGet(known -> Math.max(known, finalBestGen)); } @@ -975,8 +982,7 @@ public void endVerification(String seed) { // Tracks the latest known repository generation in a best-effort way to detect inconsistent listing of root level index-N blobs // and concurrent modifications. - // Protected for use in MockEventuallyConsistentRepository - protected final AtomicLong latestKnownRepoGen = new AtomicLong(RepositoryData.EMPTY_REPO_GEN); + private final AtomicLong latestKnownRepoGen = new AtomicLong(RepositoryData.EMPTY_REPO_GEN); @Override public void getRepositoryData(ActionListener listener) { @@ -1042,38 +1048,92 @@ public boolean isReadOnly() { } /** + * Writing a new index generation is a three step process. + * First, the {@link RepositoryMetaData} entry for this repository is set into a pending state by incrementing its + * pending generation {@code P} while its safe generation {@code N} remains unchanged. + * Second, the updated {@link RepositoryData} is written to generation {@code P + 1}. + * Lastly, the {@link RepositoryMetaData} entry for this repository is updated to the new generation {@code P + 1} and thus + * pending and safe generation are set to the same value marking the end of the update of the repository data. + * * @param repositoryData RepositoryData to write * @param expectedGen expected repository generation at the start of the operation * @param writeShardGens whether to write {@link ShardGenerations} to the new {@link RepositoryData} blob * @param listener completion listener */ protected void writeIndexGen(RepositoryData repositoryData, long expectedGen, boolean writeShardGens, ActionListener listener) { - ActionListener.completeWith(listener, () -> { - assert isReadOnly() == false; // can not write to a read only repository - final long currentGen = repositoryData.getGenId(); - if (currentGen != expectedGen) { - // the index file was updated by a concurrent operation, so we were operating on stale - // repository data - throw new RepositoryException(metadata.name(), - "concurrent modification of the index-N file, expected current generation [" + expectedGen + - "], actual current generation [" + currentGen + "] - possibly due to simultaneous snapshot deletion requests"); - } - final long newGen = currentGen + 1; + assert isReadOnly() == false; // can not write to a read only repository + final long currentGen = repositoryData.getGenId(); + if (currentGen != expectedGen) { + // the index file was updated by a concurrent operation, so we were operating on stale + // repository data + listener.onFailure(new RepositoryException(metadata.name(), + "concurrent modification of the index-N file, expected current generation [" + expectedGen + + "], actual current generation [" + currentGen + "]")); + return; + } + + // Step 1: Set repository generation state to the next possible pending generation + final StepListener setPendingStep = new StepListener<>(); + clusterService.submitStateUpdateTask("set pending repository generation [" + metadata.name() + "][" + expectedGen + "]", + new ClusterStateUpdateTask() { + + private long newGen; + + @Override + public ClusterState execute(ClusterState currentState) { + final RepositoryMetaData meta = getRepoMetaData(currentState); + final String repoName = metadata.name(); + final long genInState = meta.generation(); + // TODO: Remove all usages of this variable, instead initialize the generation when loading RepositoryData + final boolean uninitializedMeta = meta.generation() == RepositoryData.UNKNOWN_REPO_GEN; + if (uninitializedMeta == false && meta.pendingGeneration() != genInState) { + logger.info("Trying to write new repository data over unfinished write, repo [{}] is at " + + "safe generation [{}] and pending generation [{}]", meta.name(), genInState, meta.pendingGeneration()); + } + assert expectedGen == RepositoryData.EMPTY_REPO_GEN || RepositoryData.UNKNOWN_REPO_GEN == meta.generation() + || expectedGen == meta.generation() : + "Expected non-empty generation [" + expectedGen + "] does not match generation tracked in [" + meta + "]"; + // If we run into the empty repo generation for the expected gen, the repo is assumed to have been cleared of + // all contents by an external process so we reset the safe generation to the empty generation. + final long safeGeneration = expectedGen == RepositoryData.EMPTY_REPO_GEN ? RepositoryData.EMPTY_REPO_GEN + : (uninitializedMeta ? expectedGen : genInState); + // Regardless of whether or not the safe generation has been reset, the pending generation always increments so that + // even if a repository has been manually cleared of all contents we will never reuse the same repository generation. + // This is motivated by the consistency behavior the S3 based blob repository implementation has to support which does + // not offer any consistency guarantees when it comes to overwriting the same blob name with different content. + newGen = uninitializedMeta ? expectedGen + 1: metadata.pendingGeneration() + 1; + assert newGen > latestKnownRepoGen.get() : "Attempted new generation [" + newGen + + "] must be larger than latest known generation [" + latestKnownRepoGen.get() + "]"; + return ClusterState.builder(currentState).metaData(MetaData.builder(currentState.getMetaData()) + .putCustom(RepositoriesMetaData.TYPE, + currentState.metaData().custom(RepositoriesMetaData.TYPE).withUpdatedGeneration( + repoName, safeGeneration, newGen)).build()).build(); + } + + @Override + public void onFailure(String source, Exception e) { + listener.onFailure( + new RepositoryException(metadata.name(), "Failed to execute cluster state update [" + source + "]", e)); + } + + @Override + public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { + setPendingStep.onResponse(newGen); + } + }); + + // Step 2: Write new index-N blob to repository and update index.latest + setPendingStep.whenComplete(newGen -> threadPool().executor(ThreadPool.Names.SNAPSHOT).execute(ActionRunnable.wrap(listener, l -> { if (latestKnownRepoGen.get() >= newGen) { throw new IllegalArgumentException( - "Tried writing generation [" + newGen + "] but repository is at least at generation [" + newGen + "] already"); + "Tried writing generation [" + newGen + "] but repository is at least at generation [" + latestKnownRepoGen.get() + + "] already"); } // write the index file final String indexBlob = INDEX_FILE_PREFIX + Long.toString(newGen); logger.debug("Repository [{}] writing new index generational blob [{}]", metadata.name(), indexBlob); writeAtomic(indexBlob, BytesReference.bytes(repositoryData.snapshotsToXContent(XContentFactory.jsonBuilder(), writeShardGens)), true); - final long latestKnownGen = latestKnownRepoGen.updateAndGet(known -> Math.max(known, newGen)); - if (newGen < latestKnownGen) { - // Don't mess up the index.latest blob - throw new IllegalStateException( - "Wrote generation [" + newGen + "] but latest known repo gen concurrently changed to [" + latestKnownGen + "]"); - } // write the current generation to the index-latest file final BytesReference genBytes; try (BytesStreamOutput bStream = new BytesStreamOutput()) { @@ -1081,18 +1141,63 @@ protected void writeIndexGen(RepositoryData repositoryData, long expectedGen, bo genBytes = bStream.bytes(); } logger.debug("Repository [{}] updating index.latest with generation [{}]", metadata.name(), newGen); + writeAtomic(INDEX_LATEST_BLOB, genBytes, false); - // delete the N-2 index file if it exists, keep the previous one around as a backup - if (newGen - 2 >= 0) { - final String oldSnapshotIndexFile = INDEX_FILE_PREFIX + Long.toString(newGen - 2); - try { - blobContainer().deleteBlobIgnoringIfNotExists(oldSnapshotIndexFile); - } catch (IOException e) { - logger.warn("Failed to clean up old index blob [{}]", oldSnapshotIndexFile); - } - } - return null; - }); + + // Step 3: Update CS to reflect new repository generation. + clusterService.submitStateUpdateTask("set safe repository generation [" + metadata.name() + "][" + newGen + "]", + new ClusterStateUpdateTask() { + @Override + public ClusterState execute(ClusterState currentState) { + final RepositoryMetaData meta = getRepoMetaData(currentState); + if (meta.generation() != expectedGen) { + throw new IllegalStateException("Tried to update repo generation to [" + newGen + + "] but saw unexpected generation in state [" + meta + "]"); + } + if (meta.pendingGeneration() != newGen) { + throw new IllegalStateException( + "Tried to update from unexpected pending repo generation [" + meta.pendingGeneration() + + "] after write to generation [" + newGen + "]"); + } + return ClusterState.builder(currentState).metaData(MetaData.builder(currentState.getMetaData()) + .putCustom(RepositoriesMetaData.TYPE, + currentState.metaData().custom(RepositoriesMetaData.TYPE).withUpdatedGeneration( + metadata.name(), newGen, newGen)).build()).build(); + } + + @Override + public void onFailure(String source, Exception e) { + l.onFailure( + new RepositoryException(metadata.name(), "Failed to execute cluster state update [" + source + "]", e)); + } + + @Override + public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { + threadPool.executor(ThreadPool.Names.SNAPSHOT).execute(ActionRunnable.run(l, () -> { + // Delete all now outdated index files up to 1000 blobs back from the new generation. + // If there are more than 1000 dangling index-N cleanup functionality on repo delete will take care of them. + // Deleting one older than the current expectedGen is done for BwC reasons as older versions used to keep + // two index-N blobs around. + final List oldIndexN = LongStream.range( + Math.max(Math.max(expectedGen - 1, 0), newGen - 1000), newGen) + .mapToObj(gen -> INDEX_FILE_PREFIX + gen) + .collect(Collectors.toList()); + try { + blobContainer().deleteBlobsIgnoringIfNotExists(oldIndexN); + } catch (IOException e) { + logger.warn("Failed to clean up old index blobs {}", oldIndexN); + } + })); + } + }); + })), listener::onFailure); + } + + private RepositoryMetaData getRepoMetaData(ClusterState state) { + final RepositoryMetaData metaData = + state.getMetaData().custom(RepositoriesMetaData.TYPE).repository(metadata.name()); + assert metaData != null; + return metaData; } /** diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/package-info.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/package-info.java index d4f3329d354b4..5e5fe84103250 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/package-info.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/package-info.java @@ -96,6 +96,9 @@ *
    *
  1. The blobstore repository stores the {@code RepositoryData} in blobs named with incrementing suffix {@code N} at {@code /index-N} * directly under the repository's root.
  2. + *
  3. For each {@link org.elasticsearch.repositories.blobstore.BlobStoreRepository} an entry of type + * {@link org.elasticsearch.cluster.metadata.RepositoryMetaData} exists in the cluster state. It tracks the current valid + * generation {@code N} as well as the latest generation that a write was attempted for.
  4. *
  5. The blobstore also stores the most recent {@code N} as a 64bit long in the blob {@code /index.latest} directly under the * repository's root.
  6. *
@@ -116,6 +119,38 @@ * * * + * + *

Writing Updated RepositoryData to the Repository

+ * + *

Writing an updated {@link org.elasticsearch.repositories.RepositoryData} to a blob store repository is an operation that uses + * the cluster state to ensure that a specific {@code index-N} blob is never accidentally overwritten in a master failover scenario. + * The specific steps to writing a new {@code index-N} blob and thus making changes from a snapshot-create or delete operation visible + * to read operations on the repository are as follows and all run on the master node:

+ * + *
    + *
  1. Write an updated value of {@link org.elasticsearch.cluster.metadata.RepositoryMetaData} for the repository that has the same + * {@link org.elasticsearch.cluster.metadata.RepositoryMetaData#generation()} as the existing entry and has a value of + * {@link org.elasticsearch.cluster.metadata.RepositoryMetaData#pendingGeneration()} one greater than the {@code pendingGeneration} of the + * existing entry.
  2. + *
  3. On the same master node, after the cluster state has been updated in the first step, write the new {@code index-N} blob and + * also update the contents of the {@code index.latest} blob. Note that updating the index.latest blob is done on a best effort + * basis and that there is a chance for a stuck master-node to overwrite the contents of the {@code index.latest} blob after a newer + * {@code index-N} has been written by another master node. This is acceptable since the contents of {@code index.latest} are not used + * during normal operation of the repository and must only be correct for purposes of mounting the contents of a + * {@link org.elasticsearch.repositories.blobstore.BlobStoreRepository} as a read-only url repository.
  4. + *
  5. After the write has finished, set the value of {@code RepositoriesState.State#generation} to the value used for + * {@code RepositoriesState.State#pendingGeneration} so that the new entry for the state of the repository has {@code generation} and + * {@code pendingGeneration} set to the same value to signalize a clean repository state with no potentially failed writes newer than the + * last valid {@code index-N} blob in the repository.
  6. + *
+ * + *

If either of the last two steps in the above fails or master fails over to a new node at any point, then a subsequent operation + * trying to write a new {@code index-N} blob will never use the same value of {@code N} used by a previous attempt. It will always start + * over at the first of the above three steps, incrementing the {@code pendingGeneration} generation before attempting a write, thus + * ensuring no overwriting of a {@code index-N} blob ever to occur. The use of the cluster state to track the latest repository generation + * {@code N} and ensuring no overwriting of {@code index-N} blobs to ever occur allows the blob store repository to properly function even + * on blob stores with neither a consistent list operation nor an atomic "write but not overwrite" operation.

+ * *

Creating a Snapshot

* *

Creating a snapshot in the repository happens in the two steps described in detail below.

@@ -160,11 +195,7 @@ * {@code /indices/${index-snapshot-uuid}/meta-${snapshot-uuid}.dat} *
  • Write the {@link org.elasticsearch.snapshots.SnapshotInfo} blob for the given snapshot to the key {@code /snap-${snapshot-uuid}.dat} * directly under the repository root.
  • - *
  • Write an updated {@code RepositoryData} blob to the key {@code /index-${N+1}} using the {@code N} determined when initializing the - * snapshot in the first step. When doing this, the implementation checks that the blob for generation {@code N + 1} has not yet been - * written to prevent concurrent updates to the repository. If the blob for {@code N + 1} already exists the execution of finalization - * stops under the assumption that a master failover occurred and the snapshot has already been finalized by the new master.
  • - *
  • Write the updated {@code /index.latest} blob containing the new repository generation {@code N + 1}.
  • + *
  • Write an updated {@code RepositoryData} blob containing the new snapshot.
  • * * *

    Deleting a Snapshot

    @@ -189,9 +220,8 @@ * blob so that it can be deleted at the end of the snapshot delete process. * * - *
  • Write an updated {@code RepositoryData} blob with the deleted snapshot removed to key {@code /index-${N+1}} directly under the - * repository root and the repository generations that were changed in the affected shards adjusted.
  • - *
  • Write an updated {@code index.latest} blob containing {@code N + 1}.
  • + *
  • Write an updated {@code RepositoryData} blob with the deleted snapshot removed and containing the updated repository generations + * that changed for the shards affected by the delete.
  • *
  • Delete the global {@code MetaData} blob {@code meta-${snapshot-uuid}.dat} stored directly under the repository root for the snapshot * as well as the {@code SnapshotInfo} blob at {@code /snap-${snapshot-uuid}.dat}.
  • *
  • Delete all unreferenced blobs previously collected when updating the shard directories. Also, remove any index folders or blobs diff --git a/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java b/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java index efe095eb9b6c2..9d69dea97f020 100644 --- a/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java @@ -103,7 +103,7 @@ public FsRepository(RepositoryMetaData metadata, Environment environment, NamedX @Override protected BlobStore createBlobStore() throws Exception { - final String location = REPOSITORIES_LOCATION_SETTING.get(metadata.settings()); + final String location = REPOSITORIES_LOCATION_SETTING.get(getMetadata().settings()); final Path locationFile = environment.resolveRepoFile(location); return new FsBlobStore(environment.settings(), locationFile, isReadOnly()); } diff --git a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java index b5d99db0a880f..432091b81e1ec 100644 --- a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java @@ -194,7 +194,7 @@ private Repository createRepository() { Settings settings = Settings.builder().put("location", randomAlphaOfLength(10)).build(); RepositoryMetaData repositoryMetaData = new RepositoryMetaData(randomAlphaOfLength(10), FsRepository.TYPE, settings); final FsRepository repository = new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), - BlobStoreTestUtil.mockClusterService()) { + BlobStoreTestUtil.mockClusterService(repositoryMetaData)) { @Override protected void assertSnapshotOrGenericThread() { // eliminate thread name check as we create repo manually diff --git a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java index 6d6248c446df1..13102182cd7b0 100644 --- a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java @@ -137,7 +137,7 @@ public void testRetrieveSnapshots() throws Exception { public void testReadAndWriteSnapshotsThroughIndexFile() throws Exception { final BlobStoreRepository repository = setupRepo(); - + final long pendingGeneration = repository.metadata.pendingGeneration(); // write to and read from a index file with no entries assertThat(ESBlobStoreRepositoryIntegTestCase.getRepositoryData(repository).getSnapshotIds().size(), equalTo(0)); final RepositoryData emptyData = RepositoryData.EMPTY; @@ -146,7 +146,7 @@ public void testReadAndWriteSnapshotsThroughIndexFile() throws Exception { assertEquals(repoData, emptyData); assertEquals(repoData.getIndices().size(), 0); assertEquals(repoData.getSnapshotIds().size(), 0); - assertEquals(0L, repoData.getGenId()); + assertEquals(pendingGeneration + 1L, repoData.getGenId()); // write to and read from an index file with snapshots but no indices repoData = addRandomSnapshotsToRepoData(repoData, false); @@ -163,27 +163,30 @@ public void testIndexGenerationalFiles() throws Exception { final BlobStoreRepository repository = setupRepo(); assertEquals(ESBlobStoreRepositoryIntegTestCase.getRepositoryData(repository), RepositoryData.EMPTY); + final long pendingGeneration = repository.metadata.pendingGeneration(); + // write to index generational file RepositoryData repositoryData = generateRandomRepoData(); writeIndexGen(repository, repositoryData, RepositoryData.EMPTY_REPO_GEN); assertThat(ESBlobStoreRepositoryIntegTestCase.getRepositoryData(repository), equalTo(repositoryData)); - assertThat(repository.latestIndexBlobId(), equalTo(0L)); - assertThat(repository.readSnapshotIndexLatestBlob(), equalTo(0L)); + final long expectedGeneration = pendingGeneration + 1L; + assertThat(repository.latestIndexBlobId(), equalTo(expectedGeneration)); + assertThat(repository.readSnapshotIndexLatestBlob(), equalTo(expectedGeneration)); // adding more and writing to a new index generational file repositoryData = addRandomSnapshotsToRepoData(ESBlobStoreRepositoryIntegTestCase.getRepositoryData(repository), true); writeIndexGen(repository, repositoryData, repositoryData.getGenId()); assertEquals(ESBlobStoreRepositoryIntegTestCase.getRepositoryData(repository), repositoryData); - assertThat(repository.latestIndexBlobId(), equalTo(1L)); - assertThat(repository.readSnapshotIndexLatestBlob(), equalTo(1L)); + assertThat(repository.latestIndexBlobId(), equalTo(expectedGeneration + 1L)); + assertThat(repository.readSnapshotIndexLatestBlob(), equalTo(expectedGeneration + 1L)); // removing a snapshot and writing to a new index generational file repositoryData = ESBlobStoreRepositoryIntegTestCase.getRepositoryData(repository).removeSnapshot( repositoryData.getSnapshotIds().iterator().next(), ShardGenerations.EMPTY); writeIndexGen(repository, repositoryData, repositoryData.getGenId()); assertEquals(ESBlobStoreRepositoryIntegTestCase.getRepositoryData(repository), repositoryData); - assertThat(repository.latestIndexBlobId(), equalTo(2L)); - assertThat(repository.readSnapshotIndexLatestBlob(), equalTo(2L)); + assertThat(repository.latestIndexBlobId(), equalTo(expectedGeneration + 2L)); + assertThat(repository.readSnapshotIndexLatestBlob(), equalTo(expectedGeneration + 2L)); } public void testRepositoryDataConcurrentModificationNotAllowed() { diff --git a/server/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreIT.java b/server/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreIT.java index a11fb9d13bc04..ab5aebe853b9e 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreIT.java +++ b/server/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreIT.java @@ -497,11 +497,10 @@ public void testSnapshotWithStuckNode() throws Exception { logger.info("--> Go through a loop of creating and deleting a snapshot to trigger repository cleanup"); client().admin().cluster().prepareCleanupRepository("test-repo").get(); - // Subtract four files that will remain in the repository: + // Expect two files to remain in the repository: // (1) index-(N+1) - // (2) index-N (because we keep the previous version) and - // (3) index-latest - assertFileCount(repo, 3); + // (2) index-latest + assertFileCount(repo, 2); logger.info("--> done"); } diff --git a/server/src/test/java/org/elasticsearch/snapshots/RepositoriesMetaDataSerializationTests.java b/server/src/test/java/org/elasticsearch/snapshots/RepositoriesMetaDataSerializationTests.java index 17ae1def2359c..c7c97077fe9b2 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/RepositoriesMetaDataSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/RepositoriesMetaDataSerializationTests.java @@ -42,7 +42,10 @@ protected Custom createTestInstance() { int numberOfRepositories = randomInt(10); List entries = new ArrayList<>(); for (int i = 0; i < numberOfRepositories; i++) { - entries.add(new RepositoryMetaData(randomAlphaOfLength(10), randomAlphaOfLength(10), randomSettings())); + // divide by 2 to not overflow when adding to this number for the pending generation below + final long generation = randomNonNegativeLong() / 2L; + entries.add(new RepositoryMetaData(randomAlphaOfLength(10), randomAlphaOfLength(10), randomSettings(), generation, + generation + randomLongBetween(0, generation))); } entries.sort(Comparator.comparing(RepositoryMetaData::name)); return new RepositoriesMetaData(entries); diff --git a/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java b/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java index 0ec69bdaa0424..e31ad37296093 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java @@ -1321,9 +1321,8 @@ public void testDeleteSnapshot() throws Exception { logger.info("--> delete the last snapshot"); client.admin().cluster().prepareDeleteSnapshot("test-repo", lastSnapshot).get(); - logger.info("--> make sure that number of files is back to what it was when the first snapshot was made, " + - "plus one because one backup index-N file should remain"); - assertFileCount(repo, numberOfFiles[0] + 1); + logger.info("--> make sure that number of files is back to what it was when the first snapshot was made"); + assertFileCount(repo, numberOfFiles[0]); } public void testGetSnapshotsNoRepos() { diff --git a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java index 0931ceb494827..880008a96b90b 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java +++ b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java @@ -282,13 +282,13 @@ public Map listBlobsByPrefix(String blobNamePrefix) { .collect(Collectors.toList()))); } - // Randomly filter out the latest /index-N blob from a listing to test that tracking of it in latestKnownRepoGen - // overrides an inconsistent listing + // Randomly filter out the index-N blobs from a listing to test that tracking of it in latestKnownRepoGen and the cluster state + // ensures consistent repository operations private Map maybeMissLatestIndexN(Map listing) { - // Only filter out latest index-N at the repo root and only as long as we're not in a forced consistent state - if (path.parent() == null && context.consistent == false && random.nextBoolean()) { + // Randomly filter out index-N blobs at the repo root to proof that we don't need them to be consistently listed + if (path.parent() == null && context.consistent == false) { final Map filtered = new HashMap<>(listing); - filtered.remove(BlobStoreRepository.INDEX_FILE_PREFIX + latestKnownRepoGen.get()); + filtered.keySet().removeIf(b -> b.startsWith(BlobStoreRepository.INDEX_FILE_PREFIX) && random.nextBoolean()); return Map.copyOf(filtered); } return listing; diff --git a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java index f1cf314e3158a..e4e6d99c6e6f0 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.settings.Settings; @@ -134,9 +135,11 @@ public void testOverwriteShardSnapBlobFails() throws IOException { public void testOverwriteSnapshotInfoBlob() { MockEventuallyConsistentRepository.Context blobStoreContext = new MockEventuallyConsistentRepository.Context(); - try (BlobStoreRepository repository = new MockEventuallyConsistentRepository( - new RepositoryMetaData("testRepo", "mockEventuallyConsistent", Settings.EMPTY), - xContentRegistry(), BlobStoreTestUtil.mockClusterService(), blobStoreContext, random())) { + final RepositoryMetaData metaData = new RepositoryMetaData("testRepo", "mockEventuallyConsistent", Settings.EMPTY); + final ClusterService clusterService = BlobStoreTestUtil.mockClusterService(metaData); + try (BlobStoreRepository repository = + new MockEventuallyConsistentRepository(metaData, xContentRegistry(), clusterService, blobStoreContext, random())) { + clusterService.addStateApplier(event -> repository.updateState(event.state())); repository.start(); // We create a snap- blob for snapshot "foo" in the first generation diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java index 66c49db542dab..12130a1dd330a 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java @@ -26,6 +26,9 @@ import org.elasticsearch.cluster.ClusterStateApplier; import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.metadata.RepositoriesMetaData; +import org.elasticsearch.cluster.metadata.RepositoryMetaData; import org.elasticsearch.cluster.service.ClusterApplierService; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; @@ -292,11 +295,29 @@ public static void assertBlobsByPrefix(BlobStoreRepository repository, BlobPath /** * Creates a mocked {@link ClusterService} for use in {@link BlobStoreRepository} related tests that mocks out all the necessary - * functionality to make {@link BlobStoreRepository} work. + * functionality to make {@link BlobStoreRepository} work. Initializes the cluster state as {@link ClusterState#EMPTY_STATE}. * * @return Mock ClusterService */ public static ClusterService mockClusterService() { + return mockClusterService(ClusterState.EMPTY_STATE); + } + + /** + * Creates a mocked {@link ClusterService} for use in {@link BlobStoreRepository} related tests that mocks out all the necessary + * functionality to make {@link BlobStoreRepository} work. Initializes the cluster state with a {@link RepositoriesMetaData} instance + * that contains the given {@code metadata}. + * + * @param metaData RepositoryMetaData to initialize the cluster state with + * @return Mock ClusterService + */ + public static ClusterService mockClusterService(RepositoryMetaData metaData) { + return mockClusterService(ClusterState.builder(ClusterState.EMPTY_STATE).metaData( + MetaData.builder().putCustom(RepositoriesMetaData.TYPE, + new RepositoriesMetaData(Collections.singletonList(metaData))).build()).build()); + } + + private static ClusterService mockClusterService(ClusterState initialState) { final ThreadPool threadPool = mock(ThreadPool.class); when(threadPool.executor(ThreadPool.Names.SNAPSHOT)).thenReturn(new SameThreadExecutorService()); when(threadPool.generic()).thenReturn(new SameThreadExecutorService()); @@ -305,7 +326,7 @@ public static ClusterService mockClusterService() { final ClusterService clusterService = mock(ClusterService.class); final ClusterApplierService clusterApplierService = mock(ClusterApplierService.class); when(clusterService.getClusterApplierService()).thenReturn(clusterApplierService); - final AtomicReference currentState = new AtomicReference<>(ClusterState.EMPTY_STATE); + final AtomicReference currentState = new AtomicReference<>(initialState); when(clusterService.state()).then(invocationOnMock -> currentState.get()); final List appliers = new CopyOnWriteArrayList<>(); doAnswer(invocation -> { diff --git a/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java b/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java index 218c6f4eecac7..6c05cc625f5cb 100644 --- a/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java +++ b/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java @@ -100,6 +100,8 @@ public long getFailureCount() { private final String randomPrefix; + private final Environment env; + private volatile boolean blockOnControlFiles; private volatile boolean blockOnDataFiles; @@ -125,9 +127,15 @@ public MockRepository(RepositoryMetaData metadata, Environment environment, blockAndFailOnWriteSnapFile = metadata.settings().getAsBoolean("block_on_snap", false); randomPrefix = metadata.settings().get("random", "default"); waitAfterUnblock = metadata.settings().getAsLong("wait_after_unblock", 0L); + env = environment; logger.info("starting mock repository with random prefix {}", randomPrefix); } + @Override + public RepositoryMetaData getMetadata() { + return overrideSettings(super.getMetadata(), env); + } + private static RepositoryMetaData overrideSettings(RepositoryMetaData metadata, Environment environment) { // TODO: use another method of testing not being able to read the test file written by the master... // this is super duper hacky diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java index 03ca8d5cfff28..dba66e0b1b1db 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java @@ -352,7 +352,8 @@ private Environment createEnvironment() { private Repository createRepository() { Settings settings = Settings.builder().put("location", randomAlphaOfLength(10)).build(); RepositoryMetaData repositoryMetaData = new RepositoryMetaData(randomAlphaOfLength(10), FsRepository.TYPE, settings); - return new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), BlobStoreTestUtil.mockClusterService()); + return new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), + BlobStoreTestUtil.mockClusterService(repositoryMetaData)); } private static void runAsSnapshot(ThreadPool pool, Runnable runnable) { From b1f4a8578bfb09917e2c9fb4fa085650421a8162 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Wed, 4 Dec 2019 12:16:36 +0000 Subject: [PATCH 066/686] [CI] Interval queries cannot be cached if they use scripts (#49824) #49793 added test coverage for interval queries that contain script filters, but did not adjust testCacheability(), which how fails occasionally when given a random interval source containing a script. This commit overrides testCacheability() to explicitly sources with and without script filters. Fixes #49821 --- .../query/IntervalQueryBuilderTests.java | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java index 5fd91848a7064..7375b5951948f 100644 --- a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java @@ -52,7 +52,7 @@ public class IntervalQueryBuilderTests extends AbstractQueryTestCase 3) { - return createRandomMatch(depth + 1); + return createRandomMatch(depth + 1, useScripts); } switch (randomInt(20)) { case 0: @@ -94,29 +94,29 @@ private IntervalsSourceProvider createRandomSource(int depth) { int orCount = randomInt(4) + 1; List orSources = new ArrayList<>(); for (int i = 0; i < orCount; i++) { - orSources.add(createRandomSource(depth + 1)); + orSources.add(createRandomSource(depth + 1, useScripts)); } - return new IntervalsSourceProvider.Disjunction(orSources, createRandomFilter(depth + 1)); + return new IntervalsSourceProvider.Disjunction(orSources, createRandomFilter(depth + 1, useScripts)); case 2: case 3: int count = randomInt(5) + 1; List subSources = new ArrayList<>(); for (int i = 0; i < count; i++) { - subSources.add(createRandomSource(depth + 1)); + subSources.add(createRandomSource(depth + 1, useScripts)); } boolean ordered = randomBoolean(); int maxGaps = randomInt(5) - 1; - IntervalsSourceProvider.IntervalFilter filter = createRandomFilter(depth + 1); + IntervalsSourceProvider.IntervalFilter filter = createRandomFilter(depth + 1, useScripts); return new IntervalsSourceProvider.Combine(subSources, ordered, maxGaps, filter); default: - return createRandomMatch(depth + 1); + return createRandomMatch(depth + 1, useScripts); } } - private IntervalsSourceProvider.IntervalFilter createRandomFilter(int depth) { + private IntervalsSourceProvider.IntervalFilter createRandomFilter(int depth, boolean useScripts) { if (depth < 3 && randomInt(20) > 18) { - if (randomBoolean()) { - return new IntervalsSourceProvider.IntervalFilter(createRandomSource(depth + 1), randomFrom(filters)); + if (useScripts == false || randomBoolean()) { + return new IntervalsSourceProvider.IntervalFilter(createRandomSource(depth + 1, false), randomFrom(filters)); } return new IntervalsSourceProvider.IntervalFilter( new Script(ScriptType.INLINE, "mockscript", "1", Collections.emptyMap())); @@ -124,7 +124,7 @@ private IntervalsSourceProvider.IntervalFilter createRandomFilter(int depth) { return null; } - private IntervalsSourceProvider createRandomMatch(int depth) { + private IntervalsSourceProvider createRandomMatch(int depth, boolean useScripts) { String useField = rarely() ? MASKED_FIELD : null; int wordCount = randomInt(4) + 1; List words = new ArrayList<>(); @@ -135,7 +135,25 @@ private IntervalsSourceProvider createRandomMatch(int depth) { boolean mOrdered = randomBoolean(); int maxMGaps = randomInt(5) - 1; String analyzer = randomFrom("simple", "keyword", "whitespace"); - return new IntervalsSourceProvider.Match(text, maxMGaps, mOrdered, analyzer, createRandomFilter(depth + 1), useField); + return new IntervalsSourceProvider.Match(text, maxMGaps, mOrdered, analyzer, createRandomFilter(depth + 1, useScripts), useField); + } + + @Override + public void testCacheability() throws IOException { + IntervalQueryBuilder queryBuilder = new IntervalQueryBuilder(STRING_FIELD_NAME, createRandomSource(0, false)); + QueryShardContext context = createShardContext(); + QueryBuilder rewriteQuery = rewriteQuery(queryBuilder, new QueryShardContext(context)); + assertNotNull(rewriteQuery.toQuery(context)); + assertTrue("query should be cacheable: " + queryBuilder.toString(), context.isCacheable()); + + IntervalsSourceProvider.IntervalFilter scriptFilter = new IntervalsSourceProvider.IntervalFilter( + new Script(ScriptType.INLINE, "mockscript", "1", Collections.emptyMap()) + ); + IntervalsSourceProvider source = new IntervalsSourceProvider.Match("text", 0, true, "simple", scriptFilter, null); + queryBuilder = new IntervalQueryBuilder(STRING_FIELD_NAME, source); + rewriteQuery = rewriteQuery(queryBuilder, new QueryShardContext(context)); + assertNotNull(rewriteQuery.toQuery(context)); + assertFalse("query with scripts should not be cacheable: " + queryBuilder.toString(), context.isCacheable()); } @Override From 88baca32ead1407734532f7c70db9a953da30d77 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Wed, 4 Dec 2019 12:19:21 +0000 Subject: [PATCH 067/686] Fix merge conflict --- .../elasticsearch/index/query/IntervalQueryBuilderTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java index 7375b5951948f..7b004db65da50 100644 --- a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java @@ -169,7 +169,7 @@ public IntervalQueryBuilder mutateInstance(IntervalQueryBuilder instance) throws if (randomBoolean()) { return new IntervalQueryBuilder(STRING_FIELD_NAME_2, instance.getSourceProvider()); } - return new IntervalQueryBuilder(STRING_FIELD_NAME, createRandomSource(0)); + return new IntervalQueryBuilder(STRING_FIELD_NAME, createRandomSource(0, true)); } public void testMatchInterval() throws IOException { From b44803fadbeae9332eb5af6a707f9dac47bf4180 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Wed, 4 Dec 2019 13:49:43 +0100 Subject: [PATCH 068/686] Add SecureSM support for newer IDEA versions (#49747) IntelliJ IDEA moved their JUnit runner to a different package. While this does not break running tests in IDEA, it leads to an ugly exception being thrown at the end of the tests: Exception in thread "main" java.lang.SecurityException: java.lang.System#exit(0) calls are not allowed at org.elasticsearch.secure_sm.SecureSM$2.run(SecureSM.java:248) at org.elasticsearch.secure_sm.SecureSM$2.run(SecureSM.java:215) at java.base/java.security.AccessController.doPrivileged(AccessController.java:310) at org.elasticsearch.secure_sm.SecureSM.innerCheckExit(SecureSM.java:215) at org.elasticsearch.secure_sm.SecureSM.checkExit(SecureSM.java:206) at java.base/java.lang.Runtime.exit(Runtime.java:111) at java.base/java.lang.System.exit(System.java:1781) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:59) This commit adds support for newer IDEA versions in SecureSM. --- .../org/elasticsearch/secure_sm/SecureSM.java | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/libs/secure-sm/src/main/java/org/elasticsearch/secure_sm/SecureSM.java b/libs/secure-sm/src/main/java/org/elasticsearch/secure_sm/SecureSM.java index 812643d2835e0..bddb111f45bb6 100644 --- a/libs/secure-sm/src/main/java/org/elasticsearch/secure_sm/SecureSM.java +++ b/libs/secure-sm/src/main/java/org/elasticsearch/secure_sm/SecureSM.java @@ -46,16 +46,16 @@ *
      *
    • {@code modifyThread} and {@code modifyThreadGroup} are required for any thread access * checks: with these permissions, access is granted as long as the thread group is - * the same or an ancestor ({@code sourceGroup.parentOf(targetGroup) == true}). + * the same or an ancestor ({@code sourceGroup.parentOf(targetGroup) == true}). *
    • code without these permissions can do very little, except to interrupt itself. It may * not even create new threads. - *
    • very special cases (like test runners) that have {@link ThreadPermission} can violate + *
    • very special cases (like test runners) that have {@link ThreadPermission} can violate * threadgroup security rules. *
    *

    * If java security debugging ({@code java.security.debug}) is enabled, and this SecurityManager * is installed, it will emit additional debugging information when threadgroup access checks fail. - * + * * @see SecurityManager#checkAccess(Thread) * @see SecurityManager#checkAccess(ThreadGroup) * @see @@ -105,8 +105,10 @@ public static SecureSM createTestSecureSM() { "com\\.carrotsearch\\.ant\\.tasks\\.junit4\\.slave\\..*", // eclipse test runner "org\\.eclipse.jdt\\.internal\\.junit\\.runner\\..*", - // intellij test runner - "com\\.intellij\\.rt\\.execution\\.junit\\..*" + // intellij test runner (before IDEA version 2019.3) + "com\\.intellij\\.rt\\.execution\\.junit\\..*", + // intellij test runner (since IDEA version 2019.3) + "com\\.intellij\\.rt\\.junit\\..*" }; // java.security.debug support @@ -122,7 +124,7 @@ public Boolean run() { } } }); - + @Override @SuppressForbidden(reason = "java.security.debug messages go to standard error") public void checkAccess(Thread t) { @@ -137,7 +139,7 @@ public void checkAccess(Thread t) { throw e; } } - + @Override @SuppressForbidden(reason = "java.security.debug messages go to standard error") public void checkAccess(ThreadGroup g) { @@ -157,7 +159,7 @@ private void debugThreadGroups(final ThreadGroup caller, final ThreadGroup targe System.err.println("access: caller group=" + caller); System.err.println("access: target group=" + target); } - + // thread permission logic private static final Permission MODIFY_THREAD_PERMISSION = new RuntimePermission("modifyThread"); @@ -168,31 +170,31 @@ protected void checkThreadAccess(Thread t) { // first, check if we can modify threads at all. checkPermission(MODIFY_THREAD_PERMISSION); - + // check the threadgroup, if its our thread group or an ancestor, its fine. final ThreadGroup source = Thread.currentThread().getThreadGroup(); final ThreadGroup target = t.getThreadGroup(); - + if (target == null) { return; // its a dead thread, do nothing. } else if (source.parentOf(target) == false) { checkPermission(MODIFY_ARBITRARY_THREAD_PERMISSION); } } - + private static final Permission MODIFY_THREADGROUP_PERMISSION = new RuntimePermission("modifyThreadGroup"); private static final Permission MODIFY_ARBITRARY_THREADGROUP_PERMISSION = new ThreadPermission("modifyArbitraryThreadGroup"); - + protected void checkThreadGroupAccess(ThreadGroup g) { Objects.requireNonNull(g); // first, check if we can modify thread groups at all. checkPermission(MODIFY_THREADGROUP_PERMISSION); - + // check the threadgroup, if its our thread group or an ancestor, its fine. final ThreadGroup source = Thread.currentThread().getThreadGroup(); final ThreadGroup target = g; - + if (source == null) { return; // we are a dead thread, do nothing } else if (source.parentOf(target) == false) { @@ -205,7 +207,7 @@ protected void checkThreadGroupAccess(ThreadGroup g) { public void checkExit(int status) { innerCheckExit(status); } - + /** * The "Uwe Schindler" algorithm. * @@ -227,7 +229,7 @@ public Void run() { exitMethodHit = className + '#' + methodName + '(' + status + ')'; continue; } - + if (exitMethodHit != null) { if (classesThatCanExit == null) { break; @@ -240,7 +242,7 @@ public Void run() { break; } } - + if (exitMethodHit == null) { // should never happen, only if JVM hides stack trace - replace by generic: exitMethodHit = "JVM exit method"; @@ -248,7 +250,7 @@ public Void run() { throw new SecurityException(exitMethodHit + " calls are not allowed"); } }); - + // we passed the stack check, delegate to super, so default policy can still deny permission: super.checkExit(status); } From cf7c77b527c39a602564340a3fb9cfe21018e45a Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 4 Dec 2019 15:54:56 +0100 Subject: [PATCH 069/686] Stop Allocating Buffers in CopyBytesSocketChannel (#49825) The way things currently work, we read up to 1M from the channel and then potentially force all of it into the `ByteBuf` passed by Netty. Since that `ByteBuf` tends to by default be `64k` in size, large reads will force the buffer to grow, completely circumventing the logic of `allocHandle`. This seems like it could break `io.netty.channel.RecvByteBufAllocator.Handle#continueReading` since that method for the fixed-size allocator does check whether the last read was equal to the attempted read size. So if we set `64k` because that's what the buffer size is, then wirte `1M` to the buffer we will stop reading on the IO loop, even though the channel may still have bytes that we can read right away. More imporatantly though, this can lead to running OOM quite easily under IO pressure as we are forcing the heap buffers passed to the read to `reallocate`. Closes #49699 --- .../org/elasticsearch/transport/CopyBytesSocketChannel.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/CopyBytesSocketChannel.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/CopyBytesSocketChannel.java index 60e63ba786536..711dcc095b517 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/CopyBytesSocketChannel.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/CopyBytesSocketChannel.java @@ -119,8 +119,9 @@ protected void doWrite(ChannelOutboundBuffer in) throws Exception { @Override protected int doReadBytes(ByteBuf byteBuf) throws Exception { final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle(); - allocHandle.attemptedBytesRead(byteBuf.writableBytes()); - ByteBuffer ioBuffer = getIoBuffer(); + int writeableBytes = Math.min(byteBuf.writableBytes(), MAX_BYTES_PER_WRITE); + allocHandle.attemptedBytesRead(writeableBytes); + ByteBuffer ioBuffer = getIoBuffer().limit(writeableBytes); int bytesRead = readFromSocketChannel(javaChannel(), ioBuffer); ioBuffer.flip(); if (bytesRead > 0) { From 42dc5a77c26bcbc7dc2c7771617a1e3face19f3f Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Wed, 4 Dec 2019 09:58:19 -0500 Subject: [PATCH 070/686] [DOCS] Reformat length token filter docs (#49805) * Adds a title abbreviation * Updates the description and adds a Lucene link * Reformats the parameters section * Adds analyze, custom analyzer, and custom filter snippets Relates to #44726. --- .../tokenfilters/length-tokenfilter.asciidoc | 176 ++++++++++++++++-- 1 file changed, 165 insertions(+), 11 deletions(-) diff --git a/docs/reference/analysis/tokenfilters/length-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/length-tokenfilter.asciidoc index e53a198df5570..4bbe60e52be26 100644 --- a/docs/reference/analysis/tokenfilters/length-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/length-tokenfilter.asciidoc @@ -1,16 +1,170 @@ [[analysis-length-tokenfilter]] -=== Length Token Filter +=== Length token filter +++++ +Length +++++ -A token filter of type `length` that removes words that are too long or -too short for the stream. +Removes tokens shorter or longer than specified character lengths. +For example, you can use the `length` filter to exclude tokens shorter than 2 +characters and tokens longer than 5 characters. -The following are settings that can be set for a `length` token filter -type: +This filter uses Lucene's +https://lucene.apache.org/core/{lucene_version_path}/analyzers-common/org/apache/lucene/analysis/miscellaneous/LengthFilter.html[LengthFilter]. -[cols="<,<",options="header",] -|=========================================================== -|Setting |Description -|`min` |The minimum number. Defaults to `0`. -|`max` |The maximum number. Defaults to `Integer.MAX_VALUE`, which is `2^31-1` or 2147483647. -|=========================================================== +[TIP] +==== +The `length` filter removes entire tokens. If you'd prefer to shorten tokens to +a specific length, use the <> filter. +==== +[[analysis-length-tokenfilter-analyze-ex]] +==== Example + +The following <> request uses the `length` +filter to remove tokens longer than 4 characters: + +[source,console] +-------------------------------------------------- +GET _analyze +{ + "tokenizer": "whitespace", + "filter": [ + { + "type": "length", + "min": 0, + "max": 4 + } + ], + "text": "the quick brown fox jumps over the lazy dog" +} +-------------------------------------------------- + +The filter produces the following tokens: + +[source,text] +-------------------------------------------------- +[ the, fox, over, the, lazy, dog ] +-------------------------------------------------- + +///////////////////// +[source,console-result] +-------------------------------------------------- +{ + "tokens": [ + { + "token": "the", + "start_offset": 0, + "end_offset": 3, + "type": "word", + "position": 0 + }, + { + "token": "fox", + "start_offset": 16, + "end_offset": 19, + "type": "word", + "position": 3 + }, + { + "token": "over", + "start_offset": 26, + "end_offset": 30, + "type": "word", + "position": 5 + }, + { + "token": "the", + "start_offset": 31, + "end_offset": 34, + "type": "word", + "position": 6 + }, + { + "token": "lazy", + "start_offset": 35, + "end_offset": 39, + "type": "word", + "position": 7 + }, + { + "token": "dog", + "start_offset": 40, + "end_offset": 43, + "type": "word", + "position": 8 + } + ] +} +-------------------------------------------------- +///////////////////// + +[[analysis-length-tokenfilter-analyzer-ex]] +==== Add to an analyzer + +The following <> request uses the +`length` filter to configure a new +<>. + +[source,console] +-------------------------------------------------- +PUT length_example +{ + "settings": { + "analysis": { + "analyzer": { + "standard_length": { + "tokenizer": "standard", + "filter": [ "length" ] + } + } + } + } +} +-------------------------------------------------- + +[[analysis-length-tokenfilter-configure-parms]] +==== Configurable parameters + +`min`:: +(Optional, integer) +Minimum character length of a token. Shorter tokens are excluded from the +output. Defaults to `0`. + +`max`:: +(Optional, integer) +Maximum character length of a token. Longer tokens are excluded from the output. +Defaults to `Integer.MAX_VALUE`, which is `2^31-1` or `2147483647`. + +[[analysis-length-tokenfilter-customize]] +==== Customize + +To customize the `length` filter, duplicate it to create the basis +for a new custom token filter. You can modify the filter using its configurable +parameters. + +For example, the following request creates a custom `length` filter that removes +tokens shorter than 2 characters and tokens longer than 10 characters: + +[source,console] +-------------------------------------------------- +PUT length_custom_example +{ + "settings": { + "analysis": { + "analyzer": { + "whitespace_length_2_to_10_char": { + "tokenizer": "whitespace", + "filter": [ "length_2_to_10_char" ] + } + }, + "filter": { + "length_2_to_10_char": { + "type": "length", + "min": 2, + "max": 10 + } + } + } + } +} +-------------------------------------------------- From 8a65b6d08795bf58dddddd47e23caf03144baf12 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 4 Dec 2019 17:22:15 +0200 Subject: [PATCH 071/686] [ML][HLRC] DF analytics setVersion and setCreateTime should not be public (#49826) `version` and `create_time` are assigned from the action itself and thus should not be able to be set from the client. --- .../client/ml/dataframe/DataFrameAnalyticsConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsConfig.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsConfig.java index d4684bde70769..0c3f98b9a46ad 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsConfig.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsConfig.java @@ -283,12 +283,12 @@ public Builder setModelMemoryLimit(ByteSizeValue modelMemoryLimit) { return this; } - public Builder setCreateTime(Instant createTime) { + Builder setCreateTime(Instant createTime) { this.createTime = createTime; return this; } - public Builder setVersion(Version version) { + Builder setVersion(Version version) { this.version = version; return this; } From 8f8e0e638e7a8f487d38b05558c89f750e3b0cd7 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Wed, 4 Dec 2019 16:50:15 +0100 Subject: [PATCH 072/686] Fix concurrent issue in SearchPhaseController (#49829) The list used by the search progress listener can be nullified by another thread that reports a query result. This change replaces the usage of this list with a new array that is synchronously modified. Closes #49778 --- .../action/search/SearchPhaseController.java | 6 +++++- .../action/search/SearchProgressListener.java | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java b/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java index b3562cc002b5b..0bd7afa7f61c2 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java @@ -39,6 +39,7 @@ import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.SearchPhaseResult; +import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregations; @@ -564,6 +565,7 @@ public InternalSearchResponse buildResponse(SearchHits hits) { * iff the buffer is exhausted. */ static final class QueryPhaseResultConsumer extends ArraySearchPhaseResults { + private final SearchShardTarget[] processedShards; private final InternalAggregations[] aggsBuffer; private final TopDocs[] topDocsBuffer; private final boolean hasAggs; @@ -600,6 +602,7 @@ private QueryPhaseResultConsumer(SearchProgressListener progressListener, Search } this.controller = controller; this.progressListener = progressListener; + this.processedShards = new SearchShardTarget[expectedResultSize]; // no need to buffer anything if we have less expected results. in this case we don't consume any results ahead of time. this.aggsBuffer = new InternalAggregations[hasAggs ? bufferSize : 0]; this.topDocsBuffer = new TopDocs[hasTopDocs ? bufferSize : 0]; @@ -636,7 +639,7 @@ private synchronized void consumeInternal(QuerySearchResult querySearchResult) { numReducePhases++; index = 1; if (hasAggs) { - progressListener.notifyPartialReduce(progressListener.searchShards(results.asList()), + progressListener.notifyPartialReduce(progressListener.searchShards(processedShards), topDocsStats.getTotalHits(), aggsBuffer[0], numReducePhases); } } @@ -650,6 +653,7 @@ private synchronized void consumeInternal(QuerySearchResult querySearchResult) { setShardIndex(topDocs.topDocs, querySearchResult.getShardIndex()); topDocsBuffer[i] = topDocs.topDocs; } + processedShards[querySearchResult.getShardIndex()] = querySearchResult.getSearchShardTarget(); } private synchronized List getRemainingAggs() { diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchProgressListener.java b/server/src/main/java/org/elasticsearch/action/search/SearchProgressListener.java index c5b3d35159491..87146719a0f52 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchProgressListener.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchProgressListener.java @@ -25,8 +25,10 @@ import org.apache.lucene.search.TotalHits; import org.elasticsearch.cluster.routing.GroupShardsIterator; import org.elasticsearch.search.SearchPhaseResult; +import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.search.aggregations.InternalAggregations; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -169,6 +171,13 @@ final List searchShards(List results) .collect(Collectors.toUnmodifiableList()); } + final List searchShards(SearchShardTarget[] results) { + return Arrays.stream(results) + .filter(Objects::nonNull) + .map(e -> new SearchShard(e.getClusterAlias(), e.getShardId())) + .collect(Collectors.toUnmodifiableList()); + } + final List searchShards(GroupShardsIterator its) { return StreamSupport.stream(its.spliterator(), false) .map(e -> new SearchShard(e.getClusterAlias(), e.shardId())) From e013b1fc71577249aded2ce97cf01f0a3a04ddcb Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Wed, 4 Dec 2019 12:44:13 -0500 Subject: [PATCH 073/686] [DOCS] Document `minimum_should_match` defaults for `bool` query (#48865) Adds documentation for the `minimum_should_match` parameter to the `bool` query docs. Includes docs for the default values: - `1` if the `bool` query includes at least one `should` clause and no `must` or `filter` clauses - `0` otherwise --- docs/reference/query-dsl/bool-query.asciidoc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/reference/query-dsl/bool-query.asciidoc b/docs/reference/query-dsl/bool-query.asciidoc index 2d84ff057415d..6594c982c44b7 100644 --- a/docs/reference/query-dsl/bool-query.asciidoc +++ b/docs/reference/query-dsl/bool-query.asciidoc @@ -60,6 +60,19 @@ POST _search } -------------------------------------------------- +[[bool-min-should-match]] +==== Using `minimum_should_match` + +You can use the `minimum_should_match` parameter to specify the number or +percentage of `should` clauses returned documents _must_ match. + +If the `bool` query includes at least one `should` clause and no `must` or +`filter` clauses, the default value is `1`. +Otherwise, the default value is `0`. + +For other valid values, see the +<>. + [[score-bool-filter]] ==== Scoring with `bool.filter` From ba453c7ecef02ce30c6e34b41e51165de6675f2e Mon Sep 17 00:00:00 2001 From: zacharymorn Date: Wed, 4 Dec 2019 11:32:01 -0800 Subject: [PATCH 074/686] Support es7 node http publish_address format (#49279) Add parsing support to node http publish_address format cname/ip:port. --- .../sniff/ElasticsearchNodesSniffer.java | 18 +++++++++-- .../ElasticsearchNodesSnifferParseTests.java | 22 ++++++++++++++ .../es6_nodes_publication_address_format.json | 30 +++++++++++++++++++ .../es7_nodes_publication_address_format.json | 30 +++++++++++++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 client/sniffer/src/test/resources/es6_nodes_publication_address_format.json create mode 100644 client/sniffer/src/test/resources/es7_nodes_publication_address_format.json diff --git a/client/sniffer/src/main/java/org/elasticsearch/client/sniff/ElasticsearchNodesSniffer.java b/client/sniffer/src/main/java/org/elasticsearch/client/sniff/ElasticsearchNodesSniffer.java index 5c947f5625ba0..e7f055bfe0101 100644 --- a/client/sniffer/src/main/java/org/elasticsearch/client/sniff/ElasticsearchNodesSniffer.java +++ b/client/sniffer/src/main/java/org/elasticsearch/client/sniff/ElasticsearchNodesSniffer.java @@ -164,9 +164,21 @@ private static Node readNode(String nodeId, JsonParser parser, Scheme scheme) th if ("http".equals(fieldName)) { while (parser.nextToken() != JsonToken.END_OBJECT) { if (parser.getCurrentToken() == JsonToken.VALUE_STRING && "publish_address".equals(parser.getCurrentName())) { - URI publishAddressAsURI = URI.create(scheme + "://" + parser.getValueAsString()); - publishedHost = new HttpHost(publishAddressAsURI.getHost(), publishAddressAsURI.getPort(), - publishAddressAsURI.getScheme()); + String address = parser.getValueAsString(); + String host; + URI publishAddressAsURI; + + // ES7 cname/ip:port format + if(address.contains("/")) { + String[] cnameAndURI = address.split("/", 2); + publishAddressAsURI = URI.create(scheme + "://" + cnameAndURI[1]); + host = cnameAndURI[0]; + } + else { + publishAddressAsURI = URI.create(scheme + "://" + address); + host = publishAddressAsURI.getHost(); + } + publishedHost = new HttpHost(host, publishAddressAsURI.getPort(), publishAddressAsURI.getScheme()); } else if (parser.currentToken() == JsonToken.START_ARRAY && "bound_address".equals(parser.getCurrentName())) { while (parser.nextToken() != JsonToken.END_ARRAY) { URI boundAddressAsURI = URI.create(scheme + "://" + parser.getValueAsString()); diff --git a/client/sniffer/src/test/java/org/elasticsearch/client/sniff/ElasticsearchNodesSnifferParseTests.java b/client/sniffer/src/test/java/org/elasticsearch/client/sniff/ElasticsearchNodesSnifferParseTests.java index edc7330c13074..6a017e32728f2 100644 --- a/client/sniffer/src/test/java/org/elasticsearch/client/sniff/ElasticsearchNodesSnifferParseTests.java +++ b/client/sniffer/src/test/java/org/elasticsearch/client/sniff/ElasticsearchNodesSnifferParseTests.java @@ -107,6 +107,28 @@ public void test6x() throws IOException { node(9207, "c2", "6.0.0", false, false, true)); } + public void testParsingPublishAddressWithPreES7Format() throws IOException { + InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("es6_nodes_publication_address_format.json"); + + HttpEntity entity = new InputStreamEntity(in, ContentType.APPLICATION_JSON); + List nodes = ElasticsearchNodesSniffer.readHosts(entity, Scheme.HTTP, new JsonFactory()); + + assertEquals("127.0.0.1", nodes.get(0).getHost().getHostName()); + assertEquals(9200, nodes.get(0).getHost().getPort()); + assertEquals("http", nodes.get(0).getHost().getSchemeName()); + } + + public void testParsingPublishAddressWithES7Format() throws IOException { + InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("es7_nodes_publication_address_format.json"); + + HttpEntity entity = new InputStreamEntity(in, ContentType.APPLICATION_JSON); + List nodes = ElasticsearchNodesSniffer.readHosts(entity, Scheme.HTTP, new JsonFactory()); + + assertEquals("elastic.test", nodes.get(0).getHost().getHostName()); + assertEquals(9200, nodes.get(0).getHost().getPort()); + assertEquals("http", nodes.get(0).getHost().getSchemeName()); + } + private Node node(int port, String name, String version, boolean master, boolean data, boolean ingest) { HttpHost host = new HttpHost("127.0.0.1", port); Set boundHosts = new HashSet<>(2); diff --git a/client/sniffer/src/test/resources/es6_nodes_publication_address_format.json b/client/sniffer/src/test/resources/es6_nodes_publication_address_format.json new file mode 100644 index 0000000000000..7ded043b81129 --- /dev/null +++ b/client/sniffer/src/test/resources/es6_nodes_publication_address_format.json @@ -0,0 +1,30 @@ +{ + "_nodes": { + "total": 8, + "successful": 8, + "failed": 0 + }, + "cluster_name": "elasticsearch", + "nodes": { + "ikXK_skVTfWkhONhldnbkw": { + "name": "m1", + "transport_address": "127.0.0.1:9300", + "host": "127.0.0.1", + "ip": "127.0.0.1", + "version": "6.0.0", + "build_hash": "8f0685b", + "roles": [ + "master", + "ingest" + ], + "attributes": { }, + "http": { + "bound_address": [ + "127.0.0.1:9200" + ], + "publish_address": "127.0.0.1:9200", + "max_content_length_in_bytes": 104857600 + } + } + } +} diff --git a/client/sniffer/src/test/resources/es7_nodes_publication_address_format.json b/client/sniffer/src/test/resources/es7_nodes_publication_address_format.json new file mode 100644 index 0000000000000..295bf3cbd2365 --- /dev/null +++ b/client/sniffer/src/test/resources/es7_nodes_publication_address_format.json @@ -0,0 +1,30 @@ +{ + "_nodes": { + "total": 8, + "successful": 8, + "failed": 0 + }, + "cluster_name": "elasticsearch", + "nodes": { + "ikXK_skVTfWkhONhldnbkw": { + "name": "m1", + "transport_address": "127.0.0.1:9300", + "host": "127.0.0.1", + "ip": "127.0.0.1", + "version": "6.0.0", + "build_hash": "8f0685b", + "roles": [ + "master", + "ingest" + ], + "attributes": { }, + "http": { + "bound_address": [ + "elastic.test:9200" + ], + "publish_address": "elastic.test/127.0.0.1:9200", + "max_content_length_in_bytes": 104857600 + } + } + } +} From acaaea25d0e7f38cbe33528b1ac195f4f207d716 Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Wed, 4 Dec 2019 11:43:55 -0800 Subject: [PATCH 075/686] Remove extraneous pass (#49797) This removes the storeSettings pass where nodes in the AST could store information they needed out of CompilerSettings for use during later passes. CompilerSettings is part of ScriptRoot which is available during the analysis pass making the storeSettings pass redundant. --- .../org/elasticsearch/painless/Compiler.java | 6 ++---- .../elasticsearch/painless/node/ANode.java | 6 ------ .../painless/node/EAssignment.java | 10 ---------- .../elasticsearch/painless/node/EBinary.java | 7 ------- .../elasticsearch/painless/node/EBool.java | 7 ------- .../elasticsearch/painless/node/EBoolean.java | 6 ------ .../painless/node/ECallLocal.java | 8 -------- .../painless/node/ECapturingFunctionRef.java | 6 ------ .../elasticsearch/painless/node/ECast.java | 6 ------ .../elasticsearch/painless/node/EComp.java | 7 ------- .../painless/node/EConditional.java | 8 -------- .../painless/node/EConstant.java | 6 ------ .../elasticsearch/painless/node/EDecimal.java | 6 ------ .../elasticsearch/painless/node/EElvis.java | 7 ------- .../painless/node/EExplicit.java | 6 ------ .../painless/node/EFunctionRef.java | 6 ------ .../painless/node/EInstanceof.java | 6 ------ .../elasticsearch/painless/node/ELambda.java | 15 +------------- .../painless/node/EListInit.java | 8 -------- .../elasticsearch/painless/node/EMapInit.java | 12 ----------- .../painless/node/ENewArray.java | 8 -------- .../painless/node/ENewArrayFunctionRef.java | 11 +--------- .../elasticsearch/painless/node/ENewObj.java | 8 -------- .../elasticsearch/painless/node/ENull.java | 6 ------ .../elasticsearch/painless/node/ENumeric.java | 6 ------ .../elasticsearch/painless/node/ERegex.java | 10 +--------- .../elasticsearch/painless/node/EStatic.java | 6 ------ .../elasticsearch/painless/node/EString.java | 6 ------ .../elasticsearch/painless/node/EUnary.java | 6 ------ .../painless/node/EVariable.java | 6 ------ .../elasticsearch/painless/node/PBrace.java | 7 ------- .../painless/node/PCallInvoke.java | 10 ---------- .../elasticsearch/painless/node/PField.java | 6 ------ .../painless/node/PSubArrayLength.java | 6 ------ .../painless/node/PSubBrace.java | 6 ------ .../painless/node/PSubCallInvoke.java | 6 ------ .../painless/node/PSubDefArray.java | 6 ------ .../painless/node/PSubDefCall.java | 6 ------ .../painless/node/PSubDefField.java | 6 ------ .../painless/node/PSubField.java | 6 ------ .../painless/node/PSubListShortcut.java | 6 ------ .../painless/node/PSubMapShortcut.java | 6 ------ .../painless/node/PSubNullSafeCallInvoke.java | 6 ------ .../painless/node/PSubNullSafeField.java | 6 ------ .../painless/node/PSubShortcut.java | 6 ------ .../elasticsearch/painless/node/SBlock.java | 8 -------- .../elasticsearch/painless/node/SBreak.java | 6 ------ .../elasticsearch/painless/node/SCatch.java | 8 -------- .../elasticsearch/painless/node/SClass.java | 16 ++------------- .../painless/node/SContinue.java | 6 ------ .../painless/node/SDeclBlock.java | 8 -------- .../painless/node/SDeclaration.java | 8 -------- .../org/elasticsearch/painless/node/SDo.java | 10 ---------- .../elasticsearch/painless/node/SEach.java | 10 ---------- .../painless/node/SExpression.java | 6 ------ .../elasticsearch/painless/node/SField.java | 6 ------ .../org/elasticsearch/painless/node/SFor.java | 20 ------------------- .../painless/node/SFunction.java | 9 ++------- .../org/elasticsearch/painless/node/SIf.java | 10 ---------- .../elasticsearch/painless/node/SIfElse.java | 14 ------------- .../elasticsearch/painless/node/SReturn.java | 8 -------- .../painless/node/SSubEachArray.java | 6 ------ .../painless/node/SSubEachIterable.java | 6 ------ .../elasticsearch/painless/node/SThrow.java | 6 ------ .../org/elasticsearch/painless/node/STry.java | 12 ----------- .../elasticsearch/painless/node/SWhile.java | 10 ---------- 66 files changed, 9 insertions(+), 501 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java index c9cfad69cea2c..f6de8be896cb3 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java @@ -211,8 +211,7 @@ Constructor compile(Loader loader, Set extractedVariables, String nam ScriptClassInfo scriptClassInfo = new ScriptClassInfo(painlessLookup, scriptClass); SClass root = Walker.buildPainlessTree(scriptClassInfo, name, source, settings, painlessLookup, null); root.extractVariables(extractedVariables); - root.storeSettings(settings); - root.analyze(painlessLookup); + root.analyze(painlessLookup, settings); Map statics = root.write(); try { @@ -244,8 +243,7 @@ byte[] compile(String name, String source, CompilerSettings settings, Printer de SClass root = Walker.buildPainlessTree(scriptClassInfo, name, source, settings, painlessLookup, debugStream); root.extractVariables(new HashSet<>()); - root.storeSettings(settings); - root.analyze(painlessLookup); + root.analyze(painlessLookup, settings); root.write(); return root.getBytes(); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ANode.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ANode.java index ca821cf05f806..d6cc2f3c18508 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ANode.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ANode.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -56,11 +55,6 @@ public abstract class ANode { this.location = Objects.requireNonNull(location); } - /** - * Store settings required for future compiler passes. - */ - abstract void storeSettings(CompilerSettings settings); - /** * Adds all variable names referenced to the variable set. *

    diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EAssignment.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EAssignment.java index 655b511471dd0..c6fcc9820420e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EAssignment.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EAssignment.java @@ -22,7 +22,6 @@ import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.DefBootstrap; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -65,15 +64,6 @@ public EAssignment(Location location, AExpression lhs, AExpression rhs, boolean this.operation = operation; } - @Override - void storeSettings(CompilerSettings settings) { - lhs.storeSettings(settings); - - if (rhs != null) { - rhs.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { lhs.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBinary.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBinary.java index 55f4a4deca4c2..7f67f16fb3b3c 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBinary.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBinary.java @@ -21,7 +21,6 @@ import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.DefBootstrap; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -60,12 +59,6 @@ public EBinary(Location location, Operation operation, AExpression left, AExpres this.right = Objects.requireNonNull(right); } - @Override - void storeSettings(CompilerSettings settings) { - left.storeSettings(settings); - right.storeSettings(settings); - } - @Override void extractVariables(Set variables) { left.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBool.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBool.java index 3012ab46617b1..c46242ff28aac 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBool.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBool.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -50,12 +49,6 @@ public EBool(Location location, Operation operation, AExpression left, AExpressi this.right = Objects.requireNonNull(right); } - @Override - void storeSettings(CompilerSettings settings) { - left.storeSettings(settings); - right.storeSettings(settings); - } - @Override void extractVariables(Set variables) { left.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBoolean.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBoolean.java index 42a3f3c46f6b4..e1c4fd05745f9 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBoolean.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBoolean.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -40,11 +39,6 @@ public EBoolean(Location location, boolean constant) { this.constant = constant; } - @Override - void storeSettings(CompilerSettings settings) { - // Do nothing. - } - @Override void extractVariables(Set variables) { // Do nothing. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java index b791589d1f836..e386f94d01b69 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -64,13 +63,6 @@ public ECallLocal(Location location, String name, List arguments) { this.arguments = Objects.requireNonNull(arguments); } - @Override - void storeSettings(CompilerSettings settings) { - for (AExpression argument : arguments) { - argument.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { for (AExpression argument : arguments) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java index 50487e41a6b7b..00f2283472e49 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.DefBootstrap; import org.elasticsearch.painless.FunctionRef; import org.elasticsearch.painless.Globals; @@ -55,11 +54,6 @@ public ECapturingFunctionRef(Location location, String variable, String call) { this.call = Objects.requireNonNull(call); } - @Override - void storeSettings(CompilerSettings settings) { - // Do nothing. - } - @Override void extractVariables(Set variables) { variables.add(variable); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECast.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECast.java index 2f9df0a5bec25..d33f37fb6049b 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECast.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECast.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -47,11 +46,6 @@ final class ECast extends AExpression { this.cast = Objects.requireNonNull(cast); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EComp.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EComp.java index 79597ac0752ae..9ec21874234a0 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EComp.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EComp.java @@ -21,7 +21,6 @@ import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.DefBootstrap; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -59,12 +58,6 @@ public EComp(Location location, Operation operation, AExpression left, AExpressi this.right = Objects.requireNonNull(right); } - @Override - void storeSettings(CompilerSettings settings) { - left.storeSettings(settings); - right.storeSettings(settings); - } - @Override void extractVariables(Set variables) { left.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConditional.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConditional.java index c7c12a56e5c3f..c8263b587bb44 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConditional.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConditional.java @@ -21,7 +21,6 @@ import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -50,13 +49,6 @@ public EConditional(Location location, AExpression condition, AExpression left, this.right = Objects.requireNonNull(right); } - @Override - void storeSettings(CompilerSettings settings) { - condition.storeSettings(settings); - left.storeSettings(settings); - right.storeSettings(settings); - } - @Override void extractVariables(Set variables) { condition.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConstant.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConstant.java index 0a7de352f41a3..8698635359b9a 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConstant.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConstant.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -41,11 +40,6 @@ final class EConstant extends AExpression { this.constant = constant; } - @Override - void storeSettings(CompilerSettings settings) { - throw new IllegalStateException("illegal tree structure"); - } - @Override void extractVariables(Set variables) { throw new IllegalStateException("Illegal tree structure."); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EDecimal.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EDecimal.java index 19e788b0c48e6..faacb3cdc0282 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EDecimal.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EDecimal.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -43,11 +42,6 @@ public EDecimal(Location location, String value) { this.value = Objects.requireNonNull(value); } - @Override - void storeSettings(CompilerSettings settings) { - // Do nothing. - } - @Override void extractVariables(Set variables) { // Do nothing. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EElvis.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EElvis.java index 3ea6d56d959de..d5e494ffa3098 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EElvis.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EElvis.java @@ -21,7 +21,6 @@ import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -48,12 +47,6 @@ public EElvis(Location location, AExpression lhs, AExpression rhs) { this.rhs = requireNonNull(rhs); } - @Override - void storeSettings(CompilerSettings settings) { - lhs.storeSettings(settings); - rhs.storeSettings(settings); - } - @Override void extractVariables(Set variables) { lhs.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EExplicit.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EExplicit.java index a83d1ddcd705d..b092aef87de42 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EExplicit.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EExplicit.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -45,11 +44,6 @@ public EExplicit(Location location, String type, AExpression child) { this.child = Objects.requireNonNull(child); } - @Override - void storeSettings(CompilerSettings settings) { - child.storeSettings(settings); - } - @Override void extractVariables(Set variables) { child.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EFunctionRef.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EFunctionRef.java index 48cd251102cd7..97b496a5985f9 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EFunctionRef.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EFunctionRef.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.FunctionRef; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -49,11 +48,6 @@ public EFunctionRef(Location location, String type, String call) { this.call = Objects.requireNonNull(call); } - @Override - void storeSettings(CompilerSettings settings) { - // do nothing - } - @Override void extractVariables(Set variables) { // do nothing diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EInstanceof.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EInstanceof.java index 4e03f59be5e60..88dc36b497cb4 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EInstanceof.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EInstanceof.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -50,11 +49,6 @@ public EInstanceof(Location location, AExpression expression, String type) { this.type = Objects.requireNonNull(type); } - @Override - void storeSettings(CompilerSettings settings) { - expression.storeSettings(settings); - } - @Override void extractVariables(Set variables) { expression.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ELambda.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ELambda.java index 7bd027c149c95..120bbdd744797 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ELambda.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ELambda.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.FunctionRef; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -68,8 +67,6 @@ public final class ELambda extends AExpression implements ILambda { private final List paramNameStrs; private final List statements; - private CompilerSettings settings; - // extracted variables required to determine captures private final Set extractedVariables; // desugared synthetic method (lambda body) @@ -92,15 +89,6 @@ public ELambda(Location location, this.extractedVariables = new HashSet<>(); } - @Override - void storeSettings(CompilerSettings settings) { - for (AStatement statement : statements) { - statement.storeSettings(settings); - } - - this.settings = settings; - } - @Override void extractVariables(Set variables) { for (AStatement statement : statements) { @@ -180,10 +168,9 @@ void analyze(ScriptRoot scriptRoot, Locals locals) { desugared = new SFunction( location, PainlessLookupUtility.typeToCanonicalTypeName(returnType), name, paramTypes, paramNames, new SBlock(location, statements), true); - desugared.storeSettings(settings); desugared.generateSignature(scriptRoot.getPainlessLookup()); desugared.analyze(scriptRoot, Locals.newLambdaScope(locals.getProgramScope(), desugared.name, returnType, - desugared.parameters, captures.size(), settings.getMaxLoopCounter())); + desugared.parameters, captures.size(), scriptRoot.getCompilerSettings().getMaxLoopCounter())); scriptRoot.getFunctionTable().addFunction(desugared.name, desugared.returnType, desugared.typeParameters, true); scriptRoot.getClassNode().addFunction(desugared); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EListInit.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EListInit.java index 75c1e10bb683b..b0ad7a1a10112 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EListInit.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EListInit.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -53,13 +52,6 @@ public EListInit(Location location, List values) { this.values = values; } - @Override - void storeSettings(CompilerSettings settings) { - for (AExpression value : values) { - value.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { for (AExpression value : values) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EMapInit.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EMapInit.java index 8107f96c77aa4..6b2c1861bf39d 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EMapInit.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EMapInit.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -55,17 +54,6 @@ public EMapInit(Location location, List keys, List val this.values = values; } - @Override - void storeSettings(CompilerSettings settings) { - for (AExpression key : keys) { - key.storeSettings(settings); - } - - for (AExpression value : values) { - value.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { for (AExpression key : keys) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArray.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArray.java index dbf506f6de5c3..03ec8a57fc02d 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArray.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArray.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -48,13 +47,6 @@ public ENewArray(Location location, String type, List arguments, bo this.initialize = initialize; } - @Override - void storeSettings(CompilerSettings settings) { - for (AExpression argument : arguments) { - argument.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { for (AExpression argument : arguments) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArrayFunctionRef.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArrayFunctionRef.java index b85e93c19dabd..f496dec4321a9 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArrayFunctionRef.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArrayFunctionRef.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.FunctionRef; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -40,8 +39,6 @@ public final class ENewArrayFunctionRef extends AExpression implements ILambda { private final String type; - private CompilerSettings settings; - private SFunction function; private FunctionRef ref; private String defPointer; @@ -52,11 +49,6 @@ public ENewArrayFunctionRef(Location location, String type) { this.type = Objects.requireNonNull(type); } - @Override - void storeSettings(CompilerSettings settings) { - this.settings = settings; - } - @Override void extractVariables(Set variables) { // do nothing @@ -69,11 +61,10 @@ void analyze(ScriptRoot scriptRoot, Locals locals) { location, type, scriptRoot.getNextSyntheticName("newarray"), Collections.singletonList("int"), Collections.singletonList("size"), new SBlock(location, Collections.singletonList(code)), true); - function.storeSettings(settings); function.generateSignature(scriptRoot.getPainlessLookup()); function.extractVariables(null); function.analyze(scriptRoot, Locals.newLambdaScope(locals.getProgramScope(), function.name, function.returnType, - function.parameters, 0, settings.getMaxLoopCounter())); + function.parameters, 0, scriptRoot.getCompilerSettings().getMaxLoopCounter())); scriptRoot.getFunctionTable().addFunction(function.name, function.returnType, function.typeParameters, true); scriptRoot.getClassNode().addFunction(function); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java index 73d3c38addb74..5f17cd9696473 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -54,13 +53,6 @@ public ENewObj(Location location, String type, List arguments) { this.arguments = Objects.requireNonNull(arguments); } - @Override - void storeSettings(CompilerSettings settings) { - for (AExpression argument : arguments) { - argument.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { for (AExpression argument : arguments) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENull.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENull.java index 0cd33ea900b4b..7520700885f9e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENull.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENull.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -40,11 +39,6 @@ public ENull(Location location) { super(location); } - @Override - void storeSettings(CompilerSettings settings) { - // do nothing - } - @Override void extractVariables(Set variables) { // Do nothing. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENumeric.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENumeric.java index 1e7dfb9276a20..4778670979c69 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENumeric.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENumeric.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -45,11 +44,6 @@ public ENumeric(Location location, String value, int radix) { this.radix = radix; } - @Override - void storeSettings(CompilerSettings settings) { - // do nothing - } - @Override void extractVariables(Set variables) { // Do nothing. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ERegex.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ERegex.java index 7ac10be445f1e..9b2b48748c826 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ERegex.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ERegex.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Constant; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -43,8 +42,6 @@ public final class ERegex extends AExpression { private final int flags; private Constant constant; - private CompilerSettings settings; - public ERegex(Location location, String pattern, String flagsString) { super(location); @@ -59,11 +56,6 @@ public ERegex(Location location, String pattern, String flagsString) { this.flags = flags; } - @Override - void storeSettings(CompilerSettings settings) { - this.settings = settings; - } - @Override void extractVariables(Set variables) { // Do nothing. @@ -71,7 +63,7 @@ void extractVariables(Set variables) { @Override void analyze(ScriptRoot scriptRoot, Locals locals) { - if (false == settings.areRegexesEnabled()) { + if (false == scriptRoot.getCompilerSettings().areRegexesEnabled()) { throw createError(new IllegalStateException("Regexes are disabled. Set [script.painless.regex.enabled] to [true] " + "in elasticsearch.yaml to allow them. Be careful though, regexes break out of Painless's protection against deep " + "recursion and long loops.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EStatic.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EStatic.java index 9bc387137e35c..d1d50a5d590eb 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EStatic.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EStatic.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -43,11 +42,6 @@ public EStatic(Location location, String type) { this.type = Objects.requireNonNull(type); } - @Override - void storeSettings(CompilerSettings settings) { - // do nothing - } - @Override void extractVariables(Set variables) { // Do nothing. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EString.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EString.java index 71802bd371b75..79120549de165 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EString.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EString.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -41,11 +40,6 @@ public EString(Location location, String string) { this.constant = Objects.requireNonNull(string); } - @Override - void storeSettings(CompilerSettings settings) { - // do nothing - } - @Override void extractVariables(Set variables) { // Do nothing. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EUnary.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EUnary.java index 6e9698e172e0b..186c11aa70f47 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EUnary.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EUnary.java @@ -21,7 +21,6 @@ import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.DefBootstrap; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -56,11 +55,6 @@ public EUnary(Location location, Operation operation, AExpression child) { this.child = Objects.requireNonNull(child); } - @Override - void storeSettings(CompilerSettings settings) { - child.storeSettings(settings); - } - @Override void extractVariables(Set variables) { child.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EVariable.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EVariable.java index ad474b56c2e37..b19be1d67c228 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EVariable.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EVariable.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Locals.Variable; @@ -47,11 +46,6 @@ public EVariable(Location location, String name) { this.name = Objects.requireNonNull(name); } - @Override - void storeSettings(CompilerSettings settings) { - // do nothing - } - @Override void extractVariables(Set variables) { variables.add(name); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PBrace.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PBrace.java index cc18088f3fe5c..f029337a3d7e3 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PBrace.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PBrace.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -49,12 +48,6 @@ public PBrace(Location location, AExpression prefix, AExpression index) { this.index = Objects.requireNonNull(index); } - @Override - void storeSettings(CompilerSettings settings) { - prefix.storeSettings(settings); - index.storeSettings(settings); - } - @Override void extractVariables(Set variables) { prefix.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java index 92e119d37e5d9..47bd54c288640 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -54,15 +53,6 @@ public PCallInvoke(Location location, AExpression prefix, String name, boolean n this.arguments = Objects.requireNonNull(arguments); } - @Override - void storeSettings(CompilerSettings settings) { - prefix.storeSettings(settings); - - for (AExpression argument : arguments) { - argument.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { prefix.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PField.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PField.java index b38ce3edb553b..a5486524585b6 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PField.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PField.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -55,11 +54,6 @@ public PField(Location location, AExpression prefix, boolean nullSafe, String va this.value = Objects.requireNonNull(value); } - @Override - void storeSettings(CompilerSettings settings) { - prefix.storeSettings(settings); - } - @Override void extractVariables(Set variables) { prefix.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubArrayLength.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubArrayLength.java index 52ad98ae23968..07f5e690e2373 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubArrayLength.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubArrayLength.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -45,11 +44,6 @@ final class PSubArrayLength extends AStoreable { this.value = Objects.requireNonNull(value); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubBrace.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubBrace.java index 6a15556f79977..a793c55f81b6c 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubBrace.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubBrace.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -45,11 +44,6 @@ final class PSubBrace extends AStoreable { this.index = Objects.requireNonNull(index); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("illegal tree structure")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubCallInvoke.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubCallInvoke.java index a2afc502a8e0a..34ff18ffec338 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubCallInvoke.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubCallInvoke.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -49,11 +48,6 @@ final class PSubCallInvoke extends AExpression { this.arguments = Objects.requireNonNull(arguments); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefArray.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefArray.java index 64f713d1e8713..8ac360b200237 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefArray.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefArray.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.DefBootstrap; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -46,11 +45,6 @@ final class PSubDefArray extends AStoreable { this.index = Objects.requireNonNull(index); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefCall.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefCall.java index b148f4c70630f..795ce7d566a49 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefCall.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefCall.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.DefBootstrap; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -55,11 +54,6 @@ final class PSubDefCall extends AExpression { this.arguments = Objects.requireNonNull(arguments); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefField.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefField.java index 2f8e5035cbde0..441762a0c58cf 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefField.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefField.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.DefBootstrap; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -46,11 +45,6 @@ final class PSubDefField extends AStoreable { this.value = Objects.requireNonNull(value); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubField.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubField.java index 371e36beab46e..b6d683edcee4e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubField.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubField.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -47,11 +46,6 @@ final class PSubField extends AStoreable { this.field = Objects.requireNonNull(field); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubListShortcut.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubListShortcut.java index e9bdbc437eb01..9471ea8c56d64 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubListShortcut.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubListShortcut.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -51,11 +50,6 @@ final class PSubListShortcut extends AStoreable { this.index = Objects.requireNonNull(index); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubMapShortcut.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubMapShortcut.java index fb537572f6314..1558d4ce94bb3 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubMapShortcut.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubMapShortcut.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -50,11 +49,6 @@ final class PSubMapShortcut extends AStoreable { this.index = Objects.requireNonNull(index); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeCallInvoke.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeCallInvoke.java index 8d02a90415095..7ce2a6a8cd889 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeCallInvoke.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeCallInvoke.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -46,11 +45,6 @@ public PSubNullSafeCallInvoke(Location location, AExpression guarded) { this.guarded = requireNonNull(guarded); } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("illegal tree structure")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeField.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeField.java index 80dbca86d0ad8..bc9a7ac3f96b4 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeField.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeField.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -41,11 +40,6 @@ public PSubNullSafeField(Location location, AStoreable guarded) { this.guarded = guarded; } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("illegal tree structure")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubShortcut.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubShortcut.java index 214ee7f429643..181a04aa02b1a 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubShortcut.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubShortcut.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -49,11 +48,6 @@ final class PSubShortcut extends AStoreable { this.setter = setter; } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SBlock.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SBlock.java index 992e23d9acf1b..effa818280668 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SBlock.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SBlock.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -46,13 +45,6 @@ public SBlock(Location location, List statements) { this.statements = Collections.unmodifiableList(statements); } - @Override - void storeSettings(CompilerSettings settings) { - for (AStatement statement : statements) { - statement.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { for (AStatement statement : statements) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SBreak.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SBreak.java index fb8522ab8f978..cc49ac7561bff 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SBreak.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SBreak.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -38,11 +37,6 @@ public SBreak(Location location) { super(location); } - @Override - void storeSettings(CompilerSettings settings) { - // do nothing - } - @Override void extractVariables(Set variables) { // Do nothing. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SCatch.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SCatch.java index 04842110e55ed..ae5e421afa18b 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SCatch.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SCatch.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Locals.Variable; @@ -56,13 +55,6 @@ public SCatch(Location location, String type, String name, SBlock block) { this.block = block; } - @Override - void storeSettings(CompilerSettings settings) { - if (block != null) { - block.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { variables.add(name); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SClass.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SClass.java index 9099dc175a507..356f56d7cf491 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SClass.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SClass.java @@ -117,19 +117,6 @@ void addField(SField field) { fields.add(field); } - @Override - public void storeSettings(CompilerSettings settings) { - for (SFunction function : functions) { - function.storeSettings(settings); - } - - for (AStatement statement : statements) { - statement.storeSettings(settings); - } - - this.settings = settings; - } - @Override public void extractVariables(Set variables) { for (SFunction function : functions) { @@ -143,7 +130,8 @@ public void extractVariables(Set variables) { extractedVariables.addAll(variables); } - public void analyze(PainlessLookup painlessLookup) { + public void analyze(PainlessLookup painlessLookup, CompilerSettings settings) { + this.settings = settings; table = new ScriptRoot(painlessLookup, settings, scriptClassInfo, this); for (SFunction function : functions) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SContinue.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SContinue.java index 54e6d5c04e27f..2a9ea5319b6a4 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SContinue.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SContinue.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -38,11 +37,6 @@ public SContinue(Location location) { super(location); } - @Override - void storeSettings(CompilerSettings settings) { - // do nothing - } - @Override void extractVariables(Set variables) { // Do nothing. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclBlock.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclBlock.java index c4b695747780d..220c9ec3d012c 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclBlock.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclBlock.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -46,13 +45,6 @@ public SDeclBlock(Location location, List declarations) { this.declarations = Collections.unmodifiableList(declarations); } - @Override - void storeSettings(CompilerSettings settings) { - for (SDeclaration declaration: declarations) { - declaration.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { for (SDeclaration declaration : declarations) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java index c6876d6ad9aff..de42c43db4ee7 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Locals.Variable; @@ -51,13 +50,6 @@ public SDeclaration(Location location, String type, String name, AExpression exp this.expression = expression; } - @Override - void storeSettings(CompilerSettings settings) { - if (expression != null) { - expression.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { variables.add(name); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDo.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDo.java index 298c29b72d9ce..a6c9c2eb30c24 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDo.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDo.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -49,15 +48,6 @@ public SDo(Location location, SBlock block, AExpression condition) { this.block = block; } - @Override - void storeSettings(CompilerSettings settings) { - condition.storeSettings(settings); - - if (block != null) { - block.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { condition.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SEach.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SEach.java index 5fa25e2db08d0..65377aaa6e59d 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SEach.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SEach.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Locals.Variable; @@ -54,15 +53,6 @@ public SEach(Location location, String type, String name, AExpression expression this.block = block; } - @Override - void storeSettings(CompilerSettings settings) { - expression.storeSettings(settings); - - if (block != null) { - block.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { variables.add(name); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SExpression.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SExpression.java index 69526210f75e1..3057cd689b07f 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SExpression.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SExpression.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -43,11 +42,6 @@ public SExpression(Location location, AExpression expression) { this.expression = Objects.requireNonNull(expression); } - @Override - void storeSettings(CompilerSettings settings) { - expression.storeSettings(settings); - } - @Override void extractVariables(Set variables) { expression.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SField.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SField.java index f12e5797cb21c..569ca608b4502 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SField.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SField.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -65,11 +64,6 @@ public Object getInstance() { return instance; } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new UnsupportedOperationException("unexpected node")); - } - @Override void extractVariables(Set variables) { throw createError(new UnsupportedOperationException("unexpected node")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFor.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFor.java index eabe03e5f1ec5..93ba16a41e1ce 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFor.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFor.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -55,25 +54,6 @@ public SFor(Location location, ANode initializer, AExpression condition, AExpres this.block = block; } - @Override - void storeSettings(CompilerSettings settings) { - if (initializer != null) { - initializer.storeSettings(settings); - } - - if (condition != null) { - condition.storeSettings(settings); - } - - if (afterthought != null) { - afterthought.storeSettings(settings); - } - - if (block != null) { - block.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { if (initializer != null) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFunction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFunction.java index 4bf6fbcd4342f..bc50268ef363d 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFunction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFunction.java @@ -78,13 +78,6 @@ public SFunction(Location location, String rtnType, String name, this.synthetic = synthetic; } - @Override - void storeSettings(CompilerSettings settings) { - block.storeSettings(settings); - - this.settings = settings; - } - @Override void extractVariables(Set variables) { // we reset the list for function scope @@ -128,6 +121,8 @@ void generateSignature(PainlessLookup painlessLookup) { @Override void analyze(ScriptRoot scriptRoot, Locals locals) { + this.settings = scriptRoot.getCompilerSettings(); + if (block.statements.isEmpty()) { throw createError(new IllegalArgumentException("Cannot generate an empty function [" + name + "].")); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIf.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIf.java index 32e1142a7f6fd..1f4350297dc3a 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIf.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIf.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -47,15 +46,6 @@ public SIf(Location location, AExpression condition, SBlock ifblock) { this.ifblock = ifblock; } - @Override - void storeSettings(CompilerSettings settings) { - condition.storeSettings(settings); - - if (ifblock != null) { - ifblock.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { condition.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIfElse.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIfElse.java index c0b6193d5c4c9..85c23977fa5a0 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIfElse.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIfElse.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -52,19 +51,6 @@ public SIfElse(Location location, AExpression condition, SBlock ifblock, SBlock this.elseblock = elseblock; } - @Override - void storeSettings(CompilerSettings settings) { - condition.storeSettings(settings); - - if (ifblock != null) { - ifblock.storeSettings(settings); - } - - if (elseblock != null) { - elseblock.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { condition.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SReturn.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SReturn.java index 88bb4ff5ea84f..cfc1dd1e6da9b 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SReturn.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SReturn.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -43,13 +42,6 @@ public SReturn(Location location, AExpression expression) { this.expression = expression; } - @Override - void storeSettings(CompilerSettings settings) { - if (expression != null) { - expression.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { if (expression != null) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachArray.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachArray.java index 9fe6c06dada1d..c0ce95ea68adb 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachArray.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachArray.java @@ -21,7 +21,6 @@ import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Locals.Variable; @@ -57,11 +56,6 @@ final class SSubEachArray extends AStatement { this.block = block; } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachIterable.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachIterable.java index db02a7e4c751f..b6b97503555b1 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachIterable.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachIterable.java @@ -21,7 +21,6 @@ import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.DefBootstrap; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; @@ -66,11 +65,6 @@ final class SSubEachIterable extends AStatement { this.block = block; } - @Override - void storeSettings(CompilerSettings settings) { - throw createError(new IllegalStateException("illegal tree structure")); - } - @Override void extractVariables(Set variables) { throw createError(new IllegalStateException("Illegal tree structure.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SThrow.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SThrow.java index e8184ccf84bde..a8309c7f7f8a7 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SThrow.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SThrow.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -43,11 +42,6 @@ public SThrow(Location location, AExpression expression) { this.expression = Objects.requireNonNull(expression); } - @Override - void storeSettings(CompilerSettings settings) { - expression.storeSettings(settings); - } - @Override void extractVariables(Set variables) { expression.extractVariables(variables); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/STry.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/STry.java index 0923666cac3a3..67b038158cbc1 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/STry.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/STry.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -49,17 +48,6 @@ public STry(Location location, SBlock block, List catches) { this.catches = Collections.unmodifiableList(catches); } - @Override - void storeSettings(CompilerSettings settings) { - if (block != null) { - block.storeSettings(settings); - } - - for (SCatch ctch : catches) { - ctch.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { if (block != null) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SWhile.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SWhile.java index 85121e2a0ff90..ab1f29fad4a5d 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SWhile.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SWhile.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Location; @@ -49,15 +48,6 @@ public SWhile(Location location, AExpression condition, SBlock block) { this.block = block; } - @Override - void storeSettings(CompilerSettings settings) { - condition.storeSettings(settings); - - if (block != null) { - block.storeSettings(settings); - } - } - @Override void extractVariables(Set variables) { condition.extractVariables(variables); From 68adad7f9f8a46b5229c0624c4aca9d588ebd995 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Wed, 4 Dec 2019 11:56:37 -0800 Subject: [PATCH 076/686] Fix task input for docker build (#49814) The docker build task depends on the docker context being built, but it was not explicitly setup as an input. This commit adds the task as an input to the docker build. relates #49613 --- distribution/docker/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/docker/build.gradle b/distribution/docker/build.gradle index da940f17b0f98..690299061c839 100644 --- a/distribution/docker/build.gradle +++ b/distribution/docker/build.gradle @@ -151,7 +151,7 @@ check.dependsOn integTest void addBuildDockerImage(final boolean oss, final boolean ubi) { final Task buildDockerImageTask = task(taskName("build", oss, ubi, "DockerImage"), type: LoggedExec) { - dependsOn taskName("copy", oss, ubi, "DockerContext") + inputs.files(tasks.named(taskName("copy", oss, ubi, "DockerContext"))) List tags if (oss) { tags = [ From 64769cc6bfd08e1ae053c2a45b6c09bc41629a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 5 Dec 2019 09:57:01 +0100 Subject: [PATCH 077/686] [DOCS] Fixes typo in the ML anomaly detection time functions docs. (#49834) --- docs/reference/ml/anomaly-detection/functions/time.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ml/anomaly-detection/functions/time.asciidoc b/docs/reference/ml/anomaly-detection/functions/time.asciidoc index 422b4e995ec73..22cab11151d1a 100644 --- a/docs/reference/ml/anomaly-detection/functions/time.asciidoc +++ b/docs/reference/ml/anomaly-detection/functions/time.asciidoc @@ -27,7 +27,7 @@ are not affected by the bucket span, but a shorter bucket span enables quicker alerting on unusual events. * Unusual events are flagged based on the previous pattern of the data, not on what we might think of as unusual based on human experience. So, if events -typically occur between 3 a.m. and 5 a.m., and event occurring at 3 p.m. is be +typically occur between 3 a.m. and 5 a.m., an event occurring at 3 p.m. is flagged as unusual. * When Daylight Saving Time starts or stops, regular events can be flagged as anomalous. This situation occurs because the actual time of the event (as From d2f37aae06294ef90291af81b0838e575cf8e141 Mon Sep 17 00:00:00 2001 From: markharwood Date: Thu, 5 Dec 2019 12:13:53 +0000 Subject: [PATCH 078/686] Remove serialisation of adjust_pure_negative default value - (#49543) Closes #49530 --- .../resources/rest-api-spec/test/search_shards/10_basic.yml | 1 - .../java/org/elasticsearch/index/query/BoolQueryBuilder.java | 4 +++- .../org/elasticsearch/index/query/BoolQueryBuilderTests.java | 1 - .../elasticsearch/index/query/NestedQueryBuilderTests.java | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search_shards/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search_shards/10_basic.yml index 653979073b707..c89873f2b2c6f 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search_shards/10_basic.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search_shards/10_basic.yml @@ -69,7 +69,6 @@ - gte: { indices.test_index.filter.bool.should.0.term.field.boost: 1.0 } - lte: { indices.test_index.filter.bool.should.1.term.field.boost: 1.0 } - gte: { indices.test_index.filter.bool.should.1.term.field.boost: 1.0 } - - match: { indices.test_index.filter.bool.adjust_pure_negative: true} - lte: { indices.test_index.filter.bool.boost: 1.0 } - gte: { indices.test_index.filter.bool.boost: 1.0 } diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 5dd7903141ddb..cbc3710779649 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -250,7 +250,9 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep doXArrayContent(FILTER, filterClauses, builder, params); doXArrayContent(MUST_NOT, mustNotClauses, builder, params); doXArrayContent(SHOULD, shouldClauses, builder, params); - builder.field(ADJUST_PURE_NEGATIVE.getPreferredName(), adjustPureNegative); + if (adjustPureNegative != ADJUST_PURE_NEGATIVE_DEFAULT) { + builder.field(ADJUST_PURE_NEGATIVE.getPreferredName(), adjustPureNegative); + } if (minimumShouldMatch != null) { builder.field(MINIMUM_SHOULD_MATCH.getPreferredName(), minimumShouldMatch); } diff --git a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java index 1a1609fe7c50e..4678e244193ca 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java @@ -263,7 +263,6 @@ public void testFromJson() throws IOException { " }" + " }" + " } ]," + - " \"adjust_pure_negative\" : true," + " \"minimum_should_match\" : \"23\"," + " \"boost\" : 42.0" + "}" + diff --git a/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java index 08cf8eedb94d3..e2d4c4b4a0ac3 100644 --- a/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java @@ -166,7 +166,6 @@ public void testFromJson() throws IOException { " }\n" + " }\n" + " } ],\n" + - " \"adjust_pure_negative\" : true,\n" + " \"boost\" : 1.0\n" + " }\n" + " },\n" + From 4cd3f9e7d171d1e2f6b5461bfa974cd1c5864540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 5 Dec 2019 14:15:19 +0100 Subject: [PATCH 079/686] [DOCS] Adds an example of preprocessing actions to the PUT DFA API docs (#49831) --- .../apis/put-dfanalytics.asciidoc | 95 ++++++++++++++++--- 1 file changed, 84 insertions(+), 11 deletions(-) diff --git a/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc index b4971fffa9c49..ddc8f35f280e6 100644 --- a/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc @@ -102,11 +102,11 @@ single number. For example, in case of age ranges, you can model the values as `analyzed_fields`:: (Optional, object) Specify `includes` and/or `excludes` patterns to select - which fields will be included in the analysis. If `analyzed_fields` is not set, - only the relevant fields will be included. For example, all the numeric fields - for {oldetection}. For the supported field types, see <>. - Also see the <> which helps understand - field selection. + which fields will be included in the analysis. If `analyzed_fields` is not + set, only the relevant fields will be included. For example, all the numeric + fields for {oldetection}. For the supported field types, see + <>. Also see the <> + which helps understand field selection. `includes`::: (Optional, array) An array of strings that defines the fields that will be @@ -142,8 +142,8 @@ single number. For example, in case of age ranges, you can model the values as that setting. For more information, see <>. `source`:: - (object) The configuration of how to source the analysis data. It requires an `index`. - Optionally, `query` and `_source` may be specified. + (object) The configuration of how to source the analysis data. It requires an + `index`. Optionally, `query` and `_source` may be specified. `index`::: (Required, string or array) Index or indices on which to perform the @@ -163,12 +163,12 @@ single number. For example, in case of age ranges, you can model the values as cannot be included in the analysis. `includes`:::: - (array) An array of strings that defines the fields that will be included in - the destination. + (array) An array of strings that defines the fields that will be + included in the destination. `excludes`:::: - (array) An array of strings that defines the fields that will be excluded - from the destination. + (array) An array of strings that defines the fields that will be + excluded from the destination. `allow_lazy_start`:: (Optional, boolean) Whether this job should be allowed to start when there @@ -187,6 +187,79 @@ single number. For example, in case of age ranges, you can model the values as ==== {api-examples-title} +[[ml-put-dfanalytics-example-preprocess]] +===== Preprocessing actions example + +The following example shows how to limit the scope of the analysis to certain +fields, specify excluded fields in the destination index, and use a query to +filter your data before analysis. + +[source,console] +-------------------------------------------------- +PUT _ml/data_frame/analytics/model-flight-delays-pre +{ + "source": { + "index": [ + "kibana_sample_data_flights" <1> + ], + "query": { <2> + "range": { + "DistanceKilometers": { + "gt": 0 + } + } + }, + "_source": { <3> + "includes": [], + "excludes": [ + "FlightDelay", + "FlightDelayType" + ] + } + }, + "dest": { <4> + "index": "df-flight-delays", + "results_field": "ml-results" + }, + "analysis": { + "regression": { + "dependent_variable": "FlightDelayMin", + "training_percent": 90 + } + }, + "analyzed_fields": { <5> + "includes": [], + "excludes": [ + "FlightNum" + ] + }, + "model_memory_limit": "100mb" +} +-------------------------------------------------- +// TEST[skip:setup kibana sample data] + +<1> The source index to analyze. +<2> This query filters out entire documents that will not be present in the +destination index. +<3> The `_source` object defines fields in the dataset that will be included or +excluded in the destination index. In this case, `includes` does not specify any +fields, so the default behavior takes place: all the fields of the source index +will included except the ones that are explicitly specified in `excludes`. +<4> Defines the destination index that contains the results of the analysis and +the fields of the source index specified in the `_source` object. Also defines +the name of the `results_field`. +<5> Specifies fields to be included in or excluded from the analysis. This does +not affect whether the fields will be present in the destination index, only +affects whether they are used in the analysis. + +In this example, we can see that all the fields of the source index are included +in the destination index except `FlightDelay` and `FlightDelayType` because +these are defined as excluded fields by the `excludes` parameter of the +`_source` object. The `FlightNum` field is included in the destination index, +however it is not included in the analysis because it is explicitly specified as +excluded field by the `excludes` parameter of the `analyzed_fields` object. + + [[ml-put-dfanalytics-example-od]] ===== {oldetection-cap} example From 91ad03015a5050c58d20272a600ad9ad51b02053 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Thu, 5 Dec 2019 10:49:09 -0700 Subject: [PATCH 080/686] Add int indicating size of transport header (#48884) Currently we do not know the size of the transport header (map of request response headers, features array, and action name). This means that we must read the entire transport message to dependably act on the headers. This commit adds an int indicating the size of the transport headers. With this addition we can act upon the headers prior to reading the entire message. --- .../transport/InboundMessage.java | 62 ++++++++++--------- .../transport/OutboundMessage.java | 31 +++++++--- .../elasticsearch/transport/TcpHeader.java | 33 ++++++++-- .../transport/TransportLogger.java | 28 ++++----- .../transport/InboundMessageTests.java | 10 +-- .../transport/TransportLoggerTests.java | 28 +++------ 6 files changed, 108 insertions(+), 84 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/InboundMessage.java b/server/src/main/java/org/elasticsearch/transport/InboundMessage.java index 681e6144ed05b..7aa0b1b6368f7 100644 --- a/server/src/main/java/org/elasticsearch/transport/InboundMessage.java +++ b/server/src/main/java/org/elasticsearch/transport/InboundMessage.java @@ -19,9 +19,7 @@ package org.elasticsearch.transport; import org.elasticsearch.Version; -import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.compress.Compressor; import org.elasticsearch.common.compress.CompressorFactory; import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -58,10 +56,6 @@ static class Reader { } InboundMessage deserialize(BytesReference reference) throws IOException { - int messageLengthBytes = reference.length(); - final int totalMessageSize = messageLengthBytes + TcpHeader.MARKER_BYTES_SIZE + TcpHeader.MESSAGE_LENGTH_SIZE; - // we have additional bytes to read, outside of the header - boolean hasMessageBytesToRead = (totalMessageSize - TcpHeader.HEADER_SIZE) > 0; StreamInput streamInput = reference.streamInput(); boolean success = false; try (ThreadContext.StoredContext existing = threadContext.stashContext()) { @@ -70,23 +64,13 @@ InboundMessage deserialize(BytesReference reference) throws IOException { Version remoteVersion = Version.fromId(streamInput.readInt()); final boolean isHandshake = TransportStatus.isHandshake(status); ensureVersionCompatibility(remoteVersion, version, isHandshake); - if (TransportStatus.isCompress(status) && hasMessageBytesToRead && streamInput.available() > 0) { - Compressor compressor = getCompressor(reference); - if (compressor == null) { - int maxToRead = Math.min(reference.length(), 10); - StringBuilder sb = new StringBuilder("stream marked as compressed, but no compressor found, first [") - .append(maxToRead).append("] content bytes out of [").append(reference.length()) - .append("] readable bytes with message size [").append(messageLengthBytes).append("] ").append("] are ["); - for (int i = 0; i < maxToRead; i++) { - sb.append(reference.get(i)).append(","); - } - sb.append("]"); - throw new IllegalStateException(sb.toString()); - } - streamInput = compressor.streamInput(streamInput); + + if (remoteVersion.onOrAfter(TcpHeader.VERSION_WITH_HEADER_SIZE)) { + // Consume the variable header size + streamInput.readInt(); + } else { + streamInput = decompressingStream(status, remoteVersion, streamInput); } - streamInput = new NamedWriteableAwareStreamInput(streamInput, namedWriteableRegistry); - streamInput.setVersion(remoteVersion); threadContext.readHeaders(streamInput); @@ -97,8 +81,17 @@ InboundMessage deserialize(BytesReference reference) throws IOException { streamInput.readStringArray(); } final String action = streamInput.readString(); + + if (remoteVersion.onOrAfter(TcpHeader.VERSION_WITH_HEADER_SIZE)) { + streamInput = decompressingStream(status, remoteVersion, streamInput); + } + streamInput = namedWriteableStream(streamInput, remoteVersion); message = new Request(threadContext, remoteVersion, status, requestId, action, streamInput); } else { + if (remoteVersion.onOrAfter(TcpHeader.VERSION_WITH_HEADER_SIZE)) { + streamInput = decompressingStream(status, remoteVersion, streamInput); + } + streamInput = namedWriteableStream(streamInput, remoteVersion); message = new Response(threadContext, remoteVersion, status, requestId, streamInput); } success = true; @@ -109,13 +102,26 @@ InboundMessage deserialize(BytesReference reference) throws IOException { } } } - } - @Nullable - static Compressor getCompressor(BytesReference message) { - final int offset = TcpHeader.REQUEST_ID_SIZE + TcpHeader.STATUS_SIZE + TcpHeader.VERSION_ID_SIZE; - return CompressorFactory.COMPRESSOR.isCompressed(message.slice(offset, message.length() - offset)) - ? CompressorFactory.COMPRESSOR : null; + static StreamInput decompressingStream(byte status, Version remoteVersion, StreamInput streamInput) throws IOException { + if (TransportStatus.isCompress(status) && streamInput.available() > 0) { + try { + StreamInput decompressor = CompressorFactory.COMPRESSOR.streamInput(streamInput); + decompressor.setVersion(remoteVersion); + return decompressor; + } catch (IllegalArgumentException e) { + throw new IllegalStateException("stream marked as compressed, but is missing deflate header"); + } + } else { + return streamInput; + } + } + + private StreamInput namedWriteableStream(StreamInput delegate, Version remoteVersion) { + NamedWriteableAwareStreamInput streamInput = new NamedWriteableAwareStreamInput(delegate, namedWriteableRegistry); + streamInput.setVersion(remoteVersion); + return streamInput; + } } @Override diff --git a/server/src/main/java/org/elasticsearch/transport/OutboundMessage.java b/server/src/main/java/org/elasticsearch/transport/OutboundMessage.java index 7653ff6b59b90..16ba7a15a539c 100644 --- a/server/src/main/java/org/elasticsearch/transport/OutboundMessage.java +++ b/server/src/main/java/org/elasticsearch/transport/OutboundMessage.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.bytes.CompositeBytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -41,20 +42,36 @@ abstract class OutboundMessage extends NetworkMessage { BytesReference serialize(BytesStreamOutput bytesStream) throws IOException { storedContext.restore(); bytesStream.setVersion(version); - bytesStream.skip(TcpHeader.HEADER_SIZE); + bytesStream.skip(TcpHeader.headerSize(version)); // The compressible bytes stream will not close the underlying bytes stream BytesReference reference; + int variableHeaderLength = -1; + final long preHeaderPosition = bytesStream.position(); + + if (version.onOrAfter(TcpHeader.VERSION_WITH_HEADER_SIZE)) { + writeVariableHeader(bytesStream); + variableHeaderLength = Math.toIntExact(bytesStream.position() - preHeaderPosition); + } + try (CompressibleBytesOutputStream stream = new CompressibleBytesOutputStream(bytesStream, TransportStatus.isCompress(status))) { stream.setVersion(version); - threadContext.writeTo(stream); + if (variableHeaderLength == -1) { + writeVariableHeader(stream); + } reference = writeMessage(stream); } + bytesStream.seek(0); - TcpHeader.writeHeader(bytesStream, requestId, status, version, reference.length() - TcpHeader.HEADER_SIZE); + final int contentSize = reference.length() - TcpHeader.headerSize(version); + TcpHeader.writeHeader(bytesStream, requestId, status, version, contentSize, variableHeaderLength); return reference; } + protected void writeVariableHeader(StreamOutput stream) throws IOException { + threadContext.writeTo(stream); + } + protected BytesReference writeMessage(CompressibleBytesOutputStream stream) throws IOException { final BytesReference zeroCopyBuffer; if (message instanceof BytesTransportRequest) { @@ -92,13 +109,13 @@ static class Request extends OutboundMessage { } @Override - protected BytesReference writeMessage(CompressibleBytesOutputStream out) throws IOException { + protected void writeVariableHeader(StreamOutput stream) throws IOException { + super.writeVariableHeader(stream); if (version.before(Version.V_8_0_0)) { // empty features array - out.writeStringArray(Strings.EMPTY_ARRAY); + stream.writeStringArray(Strings.EMPTY_ARRAY); } - out.writeString(action); - return super.writeMessage(out); + stream.writeString(action); } private static byte setStatus(boolean compress, boolean isHandshake, Writeable message) { diff --git a/server/src/main/java/org/elasticsearch/transport/TcpHeader.java b/server/src/main/java/org/elasticsearch/transport/TcpHeader.java index d18f62f60ee45..dfa79d240e470 100644 --- a/server/src/main/java/org/elasticsearch/transport/TcpHeader.java +++ b/server/src/main/java/org/elasticsearch/transport/TcpHeader.java @@ -25,7 +25,11 @@ import java.io.IOException; public class TcpHeader { - public static final int MARKER_BYTES_SIZE = 2 * 1; + + // TODO: Change to 7.6 after backport + public static final Version VERSION_WITH_HEADER_SIZE = Version.V_8_0_0; + + public static final int MARKER_BYTES_SIZE = 2; public static final int MESSAGE_LENGTH_SIZE = 4; @@ -35,15 +39,36 @@ public class TcpHeader { public static final int VERSION_ID_SIZE = 4; - public static final int HEADER_SIZE = MARKER_BYTES_SIZE + MESSAGE_LENGTH_SIZE + REQUEST_ID_SIZE + STATUS_SIZE + VERSION_ID_SIZE; + public static final int VARIABLE_HEADER_SIZE = 4; + + private static final int PRE_76_HEADER_SIZE = MARKER_BYTES_SIZE + MESSAGE_LENGTH_SIZE + REQUEST_ID_SIZE + STATUS_SIZE + VERSION_ID_SIZE; + + private static final int HEADER_SIZE = PRE_76_HEADER_SIZE + VARIABLE_HEADER_SIZE; + + public static int headerSize(Version version) { + if (version.onOrAfter(VERSION_WITH_HEADER_SIZE)) { + return HEADER_SIZE; + } else { + return PRE_76_HEADER_SIZE; + } + } - public static void writeHeader(StreamOutput output, long requestId, byte status, Version version, int messageSize) throws IOException { + public static void writeHeader(StreamOutput output, long requestId, byte status, Version version, int contentSize, + int variableHeaderSize) throws IOException { output.writeByte((byte)'E'); output.writeByte((byte)'S'); // write the size, the size indicates the remaining message size, not including the size int - output.writeInt(messageSize + REQUEST_ID_SIZE + STATUS_SIZE + VERSION_ID_SIZE); + if (version.onOrAfter(VERSION_WITH_HEADER_SIZE)) { + output.writeInt(contentSize + REQUEST_ID_SIZE + STATUS_SIZE + VERSION_ID_SIZE + VARIABLE_HEADER_SIZE); + } else { + output.writeInt(contentSize + REQUEST_ID_SIZE + STATUS_SIZE + VERSION_ID_SIZE); + } output.writeLong(requestId); output.writeByte(status); output.writeInt(version.id); + if (version.onOrAfter(VERSION_WITH_HEADER_SIZE)) { + assert variableHeaderSize != -1 : "Variable header size not set"; + output.writeInt(variableHeaderSize); + } } } diff --git a/server/src/main/java/org/elasticsearch/transport/TransportLogger.java b/server/src/main/java/org/elasticsearch/transport/TransportLogger.java index 09f22da595635..310819969b718 100644 --- a/server/src/main/java/org/elasticsearch/transport/TransportLogger.java +++ b/server/src/main/java/org/elasticsearch/transport/TransportLogger.java @@ -22,8 +22,6 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.Version; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.compress.Compressor; -import org.elasticsearch.common.compress.NotCompressedException; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.internal.io.IOUtils; @@ -77,26 +75,24 @@ private static String format(TcpChannel channel, BytesReference message, String final byte status = streamInput.readByte(); final boolean isRequest = TransportStatus.isRequest(status); final String type = isRequest ? "request" : "response"; - final String version = Version.fromId(streamInput.readInt()).toString(); + Version version = Version.fromId(streamInput.readInt()); sb.append(" [length: ").append(messageLengthWithHeader); sb.append(", request id: ").append(requestId); sb.append(", type: ").append(type); sb.append(", version: ").append(version); - if (isRequest) { - if (TransportStatus.isCompress(status)) { - Compressor compressor; - compressor = InboundMessage.getCompressor(message); - if (compressor == null) { - throw new IllegalStateException(new NotCompressedException()); - } - streamInput = compressor.streamInput(streamInput); - } + if (version.onOrAfter(TcpHeader.VERSION_WITH_HEADER_SIZE)) { + sb.append(", header size: ").append(streamInput.readInt()).append('B'); + } else { + streamInput = InboundMessage.Reader.decompressingStream(status, version, streamInput); + } + + // read and discard headers + ThreadContext.readHeadersFromStream(streamInput); - // read and discard headers - ThreadContext.readHeadersFromStream(streamInput); - if (streamInput.getVersion().before(Version.V_8_0_0)) { - // discard the features + if (isRequest) { + if (version.before(Version.V_8_0_0)) { + // discard features streamInput.readStringArray(); } sb.append(", action: ").append(streamInput.readString()); diff --git a/server/src/test/java/org/elasticsearch/transport/InboundMessageTests.java b/server/src/test/java/org/elasticsearch/transport/InboundMessageTests.java index aa5233c77651d..e699c54ae0ca3 100644 --- a/server/src/test/java/org/elasticsearch/transport/InboundMessageTests.java +++ b/server/src/test/java/org/elasticsearch/transport/InboundMessageTests.java @@ -32,9 +32,7 @@ import org.hamcrest.Matchers; import java.io.IOException; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; public class InboundMessageTests extends ESTestCase { @@ -42,7 +40,6 @@ public class InboundMessageTests extends ESTestCase { private final NamedWriteableRegistry registry = new NamedWriteableRegistry(Collections.emptyList()); public void testReadRequest() throws IOException { - String[] features = {"feature1", "feature2"}; String value = randomAlphaOfLength(10); Message message = new Message(value); String action = randomAlphaOfLength(10); @@ -81,7 +78,6 @@ public void testReadRequest() throws IOException { } public void testReadResponse() throws IOException { - HashSet features = new HashSet<>(Arrays.asList("feature1", "feature2")); String value = randomAlphaOfLength(10); Message message = new Message(value); long requestId = randomLong(); @@ -118,7 +114,6 @@ public void testReadResponse() throws IOException { } public void testReadErrorResponse() throws IOException { - HashSet features = new HashSet<>(Arrays.asList("feature1", "feature2")); RemoteTransportException exception = new RemoteTransportException("error", new IOException()); long requestId = randomLong(); boolean isHandshake = randomBoolean(); @@ -190,18 +185,17 @@ public void testThrowOnNotCompressed() throws Exception { reference = request.serialize(streamOutput); } final byte[] serialized = BytesReference.toBytes(reference); - final int statusPosition = TcpHeader.HEADER_SIZE - TcpHeader.VERSION_ID_SIZE - 1; + final int statusPosition = TcpHeader.headerSize(Version.CURRENT) - TcpHeader.VERSION_ID_SIZE - TcpHeader.VARIABLE_HEADER_SIZE - 1; // force status byte to signal compressed on the otherwise uncompressed message serialized[statusPosition] = TransportStatus.setCompress(serialized[statusPosition]); reference = new BytesArray(serialized); InboundMessage.Reader reader = new InboundMessage.Reader(Version.CURRENT, registry, threadContext); BytesReference sliced = reference.slice(6, reference.length() - 6); final IllegalStateException iste = expectThrows(IllegalStateException.class, () -> reader.deserialize(sliced)); - assertThat(iste.getMessage(), Matchers.startsWith("stream marked as compressed, but no compressor found,")); + assertThat(iste.getMessage(), Matchers.equalTo("stream marked as compressed, but is missing deflate header")); } private void testVersionIncompatibility(Version version, Version currentVersion, boolean isHandshake) throws IOException { - String[] features = {}; String value = randomAlphaOfLength(10); Message message = new Message(value); String action = randomAlphaOfLength(10); diff --git a/server/src/test/java/org/elasticsearch/transport/TransportLoggerTests.java b/server/src/test/java/org/elasticsearch/transport/TransportLoggerTests.java index 1d62e3d9ab663..9309f115aef76 100644 --- a/server/src/test/java/org/elasticsearch/transport/TransportLoggerTests.java +++ b/server/src/test/java/org/elasticsearch/transport/TransportLoggerTests.java @@ -24,7 +24,6 @@ import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; import org.elasticsearch.action.admin.cluster.stats.ClusterStatsRequest; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.bytes.CompositeBytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; @@ -61,6 +60,7 @@ public void testLoggingHandler() throws IOException { ", request id: \\d+" + ", type: request" + ", version: .*" + + ", header size: \\d+B" + ", action: cluster:monitor/stats]" + " WRITE: \\d+B"; final MockLogAppender.LoggingExpectation writeExpectation = @@ -72,6 +72,7 @@ public void testLoggingHandler() throws IOException { ", request id: \\d+" + ", type: request" + ", version: .*" + + ", header size: \\d+B" + ", action: cluster:monitor/stats]" + " READ: \\d+B"; @@ -88,26 +89,11 @@ public void testLoggingHandler() throws IOException { } private BytesReference buildRequest() throws IOException { - try (BytesStreamOutput messageOutput = new BytesStreamOutput()) { - messageOutput.setVersion(Version.CURRENT); - ThreadContext context = new ThreadContext(Settings.EMPTY); - context.writeTo(messageOutput); - messageOutput.writeString(ClusterStatsAction.NAME); - new ClusterStatsRequest().writeTo(messageOutput); - BytesReference messageBody = messageOutput.bytes(); - final BytesReference header = buildHeader(randomInt(30), messageBody.length()); - return new CompositeBytesReference(header, messageBody); - } - } - - private BytesReference buildHeader(long requestId, int length) throws IOException { - try (BytesStreamOutput headerOutput = new BytesStreamOutput(TcpHeader.HEADER_SIZE)) { - headerOutput.setVersion(Version.CURRENT); - TcpHeader.writeHeader(headerOutput, requestId, TransportStatus.setRequest((byte) 0), Version.CURRENT, length); - final BytesReference bytes = headerOutput.bytes(); - assert bytes.length() == TcpHeader.HEADER_SIZE : "header size mismatch expected: " + TcpHeader.HEADER_SIZE + " but was: " - + bytes.length(); - return bytes; + boolean compress = randomBoolean(); + try (BytesStreamOutput bytesStreamOutput = new BytesStreamOutput()) { + OutboundMessage.Request request = new OutboundMessage.Request(new ThreadContext(Settings.EMPTY), new ClusterStatsRequest(), + Version.CURRENT, ClusterStatsAction.NAME, randomInt(30), false, compress); + return request.serialize(bytesStreamOutput); } } } From 3ffd4369162c68efac55582d83cfb7282390d069 Mon Sep 17 00:00:00 2001 From: Orhan Toy Date: Thu, 5 Dec 2019 20:24:22 +0100 Subject: [PATCH 081/686] [DOCS] Minor typo fixes in reindex.asciidoc (#49863) --- docs/reference/docs/reindex.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/docs/reindex.asciidoc b/docs/reference/docs/reindex.asciidoc index c5ba1d36743fc..ae7f3f1939896 100644 --- a/docs/reference/docs/reindex.asciidoc +++ b/docs/reference/docs/reindex.asciidoc @@ -549,7 +549,7 @@ the reindex returned a `noop` value for `ctx.op`. `version_conflicts`:: -{integer)The number of version conflicts that reindex hit. +(integer) The number of version conflicts that reindex hits. `retries`:: From ca77ebfa7a0bf8c8a57c2ad759fab5dc821a6a59 Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Thu, 5 Dec 2019 12:08:23 -0800 Subject: [PATCH 082/686] Minor Painless Clean Up (#49844) This cleans up two minor things. - Cleans up style of == false - Pulls maxLoopCounter into a member variable instead of accessing CompilerSettings multiple times in the SFunction node --- .../java/org/elasticsearch/painless/node/ERegex.java | 2 +- .../org/elasticsearch/painless/node/SFunction.java | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ERegex.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ERegex.java index 9b2b48748c826..210894e86fa4c 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ERegex.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ERegex.java @@ -63,7 +63,7 @@ void extractVariables(Set variables) { @Override void analyze(ScriptRoot scriptRoot, Locals locals) { - if (false == scriptRoot.getCompilerSettings().areRegexesEnabled()) { + if (scriptRoot.getCompilerSettings().areRegexesEnabled() == false) { throw createError(new IllegalStateException("Regexes are disabled. Set [script.painless.regex.enabled] to [true] " + "in elasticsearch.yaml to allow them. Be careful though, regexes break out of Painless's protection against deep " + "recursion and long loops.")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFunction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFunction.java index bc50268ef363d..75122903ef12f 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFunction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFunction.java @@ -20,7 +20,6 @@ package org.elasticsearch.painless.node; import org.elasticsearch.painless.ClassWriter; -import org.elasticsearch.painless.CompilerSettings; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Locals.Parameter; @@ -54,7 +53,7 @@ public final class SFunction extends AStatement { private final SBlock block; public final boolean synthetic; - private CompilerSettings settings; + private int maxLoopCounter; Class returnType; List> typeParameters; @@ -121,7 +120,7 @@ void generateSignature(PainlessLookup painlessLookup) { @Override void analyze(ScriptRoot scriptRoot, Locals locals) { - this.settings = scriptRoot.getCompilerSettings(); + maxLoopCounter = scriptRoot.getCompilerSettings().getMaxLoopCounter(); if (block.statements.isEmpty()) { throw createError(new IllegalArgumentException("Cannot generate an empty function [" + name + "].")); @@ -137,7 +136,7 @@ void analyze(ScriptRoot scriptRoot, Locals locals) { throw createError(new IllegalArgumentException("Not all paths provide a return value for method [" + name + "].")); } - if (settings.getMaxLoopCounter() > 0) { + if (maxLoopCounter > 0) { loop = locals.getVariable(null, Locals.LOOP); } } @@ -156,10 +155,10 @@ void write(ClassWriter classWriter, Globals globals) { @Override void write(ClassWriter classWriter, MethodWriter methodWriter, Globals globals) { - if (settings.getMaxLoopCounter() > 0) { + if (maxLoopCounter > 0) { // if there is infinite loop protection, we do this once: // int #loop = settings.getMaxLoopCounter() - methodWriter.push(settings.getMaxLoopCounter()); + methodWriter.push(maxLoopCounter); methodWriter.visitVarInsn(Opcodes.ISTORE, loop.getSlot()); } From f95a56cc1fc718d349f4c5674eda77e4a4caa348 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Thu, 5 Dec 2019 13:03:06 -0800 Subject: [PATCH 083/686] Fix external integ test zip dep to expect a zip (#49813) When external plugin authors use build-tools, their integ tests depend on the integ-test-zip artifact. However, this dependency was broken in 7.5.0 by accidentally removing the `@zip` qualifier on the maven dependency, which works around the fact the pom for the integ-test-zip claims the artifact is a pom instead of zip packaging. This commit restores the workaround of using `@zip` until the pom can be fixed. closes #49787 --- .../org/elasticsearch/gradle/DistributionDownloadPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/DistributionDownloadPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/DistributionDownloadPlugin.java index c26576b2a1914..88cfb80a5a2d7 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/DistributionDownloadPlugin.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/DistributionDownloadPlugin.java @@ -212,7 +212,7 @@ private Object dependencyNotation(Project project, ElasticsearchDistribution dis } if (distribution.getType() == Type.INTEG_TEST_ZIP) { - return "org.elasticsearch.distribution.integ-test-zip:elasticsearch:" + distribution.getVersion(); + return "org.elasticsearch.distribution.integ-test-zip:elasticsearch:" + distribution.getVersion() + "@zip"; } From 6a67e55e8fd021d41ba8f63f92f15751cd2572b5 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Thu, 5 Dec 2019 16:11:27 -0500 Subject: [PATCH 084/686] Decouple pipeline reductions from final agg reduction (#45796) Historically only two things happened in the final reduction: empty buckets were filled, and pipeline aggs were reduced (since it was the final reduction, this was safe). Usage of the final reduction is growing however. Auto-date-histo might need to perform many reductions on final-reduce to merge down buckets, CCS may need to side-step the final reduction if sending to a different cluster, etc Having pipelines generate their output in the final reduce was convenient, but is becoming increasingly difficult to manage as the rest of the agg framework advances. This commit decouples pipeline aggs from the final reduction by introducing a new "top level" reduce, which should be called at the beginning of the reduce cycle (e.g. from the SearchPhaseController). This will only reduce pipeline aggs on the final reduce after the non-pipeline agg tree has been fully reduced. By separating pipeline reduction into their own set of methods, aggregations are free to use the final reduction for whatever purpose without worrying about generating pipeline results which are non-reducible --- .../matrix/stats/InternalMatrixStats.java | 2 +- .../action/search/SearchPhaseController.java | 4 +- .../action/search/SearchResponseMerger.java | 2 +- .../aggregations/InternalAggregation.java | 24 +++++---- .../aggregations/InternalAggregations.java | 47 ++++++++++++++--- .../InternalMultiBucketAggregation.java | 27 +++++++++- .../InternalSingleBucketAggregation.java | 2 +- .../adjacency/InternalAdjacencyMatrix.java | 2 +- .../bucket/composite/InternalComposite.java | 2 +- .../bucket/filter/InternalFilters.java | 2 +- .../bucket/geogrid/InternalGeoGrid.java | 2 +- .../histogram/InternalAutoDateHistogram.java | 2 +- .../histogram/InternalDateHistogram.java | 2 +- .../bucket/histogram/InternalHistogram.java | 2 +- .../bucket/range/InternalBinaryRange.java | 2 +- .../bucket/range/InternalRange.java | 2 +- .../bucket/sampler/UnmappedSampler.java | 2 +- ...balOrdinalsSignificantTermsAggregator.java | 2 +- .../significant/InternalSignificantTerms.java | 5 +- .../significant/SignificantLongTerms.java | 11 ++-- .../SignificantLongTermsAggregator.java | 2 +- .../significant/SignificantStringTerms.java | 13 ++--- .../SignificantStringTermsAggregator.java | 2 +- .../SignificantTextAggregator.java | 2 +- .../significant/UnmappedSignificantTerms.java | 2 +- .../bucket/terms/DoubleTerms.java | 6 +-- .../bucket/terms/InternalMappedRareTerms.java | 2 +- .../bucket/terms/InternalRareTerms.java | 2 +- .../bucket/terms/InternalTerms.java | 2 +- .../aggregations/bucket/terms/LongTerms.java | 6 +-- .../bucket/terms/UnmappedRareTerms.java | 2 +- .../bucket/terms/UnmappedTerms.java | 2 +- .../AbstractInternalHDRPercentiles.java | 2 +- .../AbstractInternalTDigestPercentiles.java | 2 +- .../aggregations/metrics/InternalAvg.java | 2 +- .../metrics/InternalCardinality.java | 2 +- .../metrics/InternalExtendedStats.java | 4 +- .../metrics/InternalGeoBounds.java | 2 +- .../metrics/InternalGeoCentroid.java | 2 +- .../aggregations/metrics/InternalMax.java | 2 +- .../InternalMedianAbsoluteDeviation.java | 2 +- .../aggregations/metrics/InternalMin.java | 2 +- .../metrics/InternalScriptedMetric.java | 2 +- .../aggregations/metrics/InternalStats.java | 2 +- .../aggregations/metrics/InternalSum.java | 2 +- .../aggregations/metrics/InternalTopHits.java | 2 +- .../metrics/InternalValueCount.java | 2 +- .../metrics/InternalWeightedAvg.java | 2 +- .../pipeline/InternalBucketMetricValue.java | 2 +- .../pipeline/InternalExtendedStatsBucket.java | 2 +- .../pipeline/InternalPercentilesBucket.java | 2 +- .../pipeline/InternalSimpleValue.java | 2 +- .../pipeline/InternalStatsBucket.java | 2 +- .../InternalAggregationsTests.java | 6 +-- .../AutoDateHistogramAggregatorTests.java | 52 ++++++++++++++++++- .../InternalAutoDateHistogramTests.java | 1 - .../histogram/InternalHistogramTests.java | 2 +- .../SignificanceHeuristicTests.java | 10 ++-- .../SignificantLongTermsTests.java | 4 +- .../SignificantStringTermsTests.java | 4 +- .../bucket/terms/TermsAggregatorTests.java | 2 +- .../metrics/InternalAvgTests.java | 4 +- .../metrics/InternalExtendedStatsTests.java | 2 +- .../metrics/InternalStatsTests.java | 2 +- .../metrics/InternalSumTests.java | 2 +- .../pipeline/AvgBucketAggregatorTests.java | 2 +- .../aggregations/AggregatorTestCase.java | 9 +++- .../InternalSimpleLongValue.java | 2 +- .../stringstats/InternalStringStats.java | 2 +- .../RollupResponseTranslationTests.java | 2 +- .../extractor/TestMultiValueAggregation.java | 2 +- .../extractor/TestSingleValueAggregation.java | 2 +- 72 files changed, 223 insertions(+), 122 deletions(-) diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/InternalMatrixStats.java b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/InternalMatrixStats.java index c70ddea7e3b22..5fda5af7418a3 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/InternalMatrixStats.java +++ b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/InternalMatrixStats.java @@ -233,7 +233,7 @@ public Object getProperty(List path) { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { // merge stats across all shards List aggs = new ArrayList<>(aggregations); aggs.removeIf(p -> ((InternalMatrixStats)p).stats == null); diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java b/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java index 0bd7afa7f61c2..27b5c9cf3b2a8 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java @@ -487,7 +487,7 @@ private ReducedQueryPhase reducedQueryPhase(Collectionmost cases, the assumption will be the all given * aggregations are of the same type (the same type as this aggregation). For best efficiency, when implementing, * try reusing an existing instance (typically the first in the given list) to save on redundant object * construction. */ - public final InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { - InternalAggregation aggResult = doReduce(aggregations, reduceContext); - if (reduceContext.isFinalReduce()) { - for (PipelineAggregator pipelineAggregator : pipelineAggregators) { - aggResult = pipelineAggregator.reduce(aggResult, reduceContext); - } - } - return aggResult; - } - - public abstract InternalAggregation doReduce(List aggregations, ReduceContext reduceContext); + public abstract InternalAggregation reduce(List aggregations, ReduceContext reduceContext); /** * Return true if this aggregation is mapped, and can lead a reduction. If this agg returns diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java b/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java index e1597c5c8c063..b4fdd74a10b85 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; /** * An internal implementation of {@link Aggregations}. @@ -91,10 +92,47 @@ public List getTopLevelPipelineAggregators() { return topLevelPipelineAggregators; } + @SuppressWarnings("unchecked") + private List getInternalAggregations() { + return (List) aggregations; + } + + /** + * Begin the reduction process. This should be the entry point for the "first" reduction, e.g. called by + * SearchPhaseController or anywhere else that wants to initiate a reduction. It _should not_ be called + * as an intermediate reduction step (e.g. in the middle of an aggregation tree). + * + * This method first reduces the aggregations, and if it is the final reduce, then reduce the pipeline + * aggregations (both embedded parent/sibling as well as top-level sibling pipelines) + */ + public static InternalAggregations topLevelReduce(List aggregationsList, ReduceContext context) { + InternalAggregations reduced = reduce(aggregationsList, context); + if (reduced == null) { + return null; + } + + if (context.isFinalReduce()) { + List reducedInternalAggs = reduced.getInternalAggregations(); + reducedInternalAggs = reducedInternalAggs.stream() + .map(agg -> agg.reducePipelines(agg, context)) + .collect(Collectors.toList()); + + List topLevelPipelineAggregators = aggregationsList.get(0).getTopLevelPipelineAggregators(); + for (SiblingPipelineAggregator pipelineAggregator : topLevelPipelineAggregators) { + InternalAggregation newAgg + = pipelineAggregator.doReduce(new InternalAggregations(reducedInternalAggs), context); + reducedInternalAggs.add(newAgg); + } + return new InternalAggregations(reducedInternalAggs); + } + return reduced; + } + /** * Reduces the given list of aggregations as well as the top-level pipeline aggregators extracted from the first * {@link InternalAggregations} object found in the list. - * Note that top-level pipeline aggregators are reduced only as part of the final reduction phase, otherwise they are left untouched. + * Note that pipeline aggregations _are not_ reduced by this method. Pipelines are handled + * separately by {@link InternalAggregations#topLevelReduce(List, ReduceContext)} */ public static InternalAggregations reduce(List aggregationsList, ReduceContext context) { if (aggregationsList.isEmpty()) { @@ -123,13 +161,6 @@ public static InternalAggregations reduce(List aggregation reducedAggregations.add(first.reduce(aggregations, context)); } - if (context.isFinalReduce()) { - for (SiblingPipelineAggregator pipelineAggregator : topLevelPipelineAggregators) { - InternalAggregation newAgg = pipelineAggregator.doReduce(new InternalAggregations(reducedAggregations), context); - reducedAggregations.add(newAgg); - } - return new InternalAggregations(reducedAggregations); - } return new InternalAggregations(reducedAggregations, topLevelPipelineAggregators); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java b/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java index 41b1a9aef6230..cfad39166e70d 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java @@ -26,6 +26,7 @@ import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -73,7 +74,7 @@ protected InternalMultiBucketAggregation(StreamInput in) throws IOException { protected abstract B reduceBucket(List buckets, ReduceContext context); @Override - public abstract List getBuckets(); + public abstract List getBuckets(); @Override public Object getProperty(List path) { @@ -141,6 +142,30 @@ public static int countInnerBucket(Aggregation agg) { return size; } + /** + * Unlike {@link InternalAggregation#reducePipelines(InternalAggregation, ReduceContext)}, a multi-bucket + * agg needs to first reduce the buckets (and their parent pipelines) before allowing sibling pipelines + * to materialize + */ + @Override + public final InternalAggregation reducePipelines(InternalAggregation reducedAggs, ReduceContext reduceContext) { + assert reduceContext.isFinalReduce(); + List materializedBuckets = reducePipelineBuckets(reduceContext); + return super.reducePipelines(create(materializedBuckets), reduceContext); + } + + private List reducePipelineBuckets(ReduceContext reduceContext) { + List reducedBuckets = new ArrayList<>(); + for (B bucket : getBuckets()) { + List aggs = new ArrayList<>(); + for (Aggregation agg : bucket.getAggregations()) { + aggs.add(((InternalAggregation)agg).reducePipelines((InternalAggregation)agg, reduceContext)); + } + reducedBuckets.add(createBucket(new InternalAggregations(aggs), bucket)); + } + return reducedBuckets; + } + public abstract static class InternalBucket implements Bucket, Writeable { public Object getProperty(String containingAggName, List path) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java index cf5ddc54884d0..0a34e7a92b8f1 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java @@ -97,7 +97,7 @@ public InternalSingleBucketAggregation create(InternalAggregations subAggregatio protected abstract InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations); @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { long docCount = 0L; List subAggregationsList = new ArrayList<>(aggregations.size()); for (InternalAggregation aggregation : aggregations) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/adjacency/InternalAdjacencyMatrix.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/adjacency/InternalAdjacencyMatrix.java index 57c7d703cbdf6..78181e3a3366a 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/adjacency/InternalAdjacencyMatrix.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/adjacency/InternalAdjacencyMatrix.java @@ -181,7 +181,7 @@ public InternalBucket getBucketByKey(String key) { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { Map> bucketsMap = new HashMap<>(); for (InternalAggregation aggregation : aggregations) { InternalAdjacencyMatrix filters = (InternalAdjacencyMatrix) aggregation; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java index e11db15acecad..243ae557bfa2c 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java @@ -143,7 +143,7 @@ int[] getReverseMuls() { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { PriorityQueue pq = new PriorityQueue<>(aggregations.size()); for (InternalAggregation agg : aggregations) { InternalComposite sortedAgg = (InternalComposite) agg; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFilters.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFilters.java index 271d1c54d5898..d99da0187f74b 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFilters.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFilters.java @@ -189,7 +189,7 @@ public InternalBucket getBucketByKey(String key) { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { List> bucketsList = null; for (InternalAggregation aggregation : aggregations) { InternalFilters filters = (InternalFilters) aggregation; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoGrid.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoGrid.java index 61c06a062cc05..1760fc19728c4 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoGrid.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoGrid.java @@ -81,7 +81,7 @@ public List getBuckets() { } @Override - public InternalGeoGrid doReduce(List aggregations, ReduceContext reduceContext) { + public InternalGeoGrid reduce(List aggregations, ReduceContext reduceContext) { LongObjectPagedHashMap> buckets = null; for (InternalAggregation aggregation : aggregations) { InternalGeoGrid grid = (InternalGeoGrid) aggregation; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogram.java index c776d764637dd..0d81188c8ca0e 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogram.java @@ -498,7 +498,7 @@ static int getAppropriateRounding(long minKey, long maxKey, int roundingIdx, } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { BucketReduceResult reducedBucketsResult = reduceBuckets(aggregations, reduceContext); if (reduceContext.isFinalReduce()) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java index 1e79b60ca7976..1f1775ede7535 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java @@ -448,7 +448,7 @@ private void addEmptyBuckets(List list, ReduceContext reduceContext) { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { List reducedBuckets = reduceBuckets(aggregations, reduceContext); if (reduceContext.isFinalReduce()) { if (minDocCount == 0) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index f4f7db5cd64a2..bc20534c5251b 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -421,7 +421,7 @@ private void addEmptyBuckets(List list, ReduceContext reduceContext) { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { List reducedBuckets = reduceBuckets(aggregations, reduceContext); if (reduceContext.isFinalReduce()) { if (minDocCount == 0) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalBinaryRange.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalBinaryRange.java index 04252c0a25a50..21d0f2fdab5ca 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalBinaryRange.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalBinaryRange.java @@ -230,7 +230,7 @@ public Bucket createBucket(InternalAggregations aggregations, Bucket prototype) } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { reduceContext.consumeBucketsAndMaybeBreak(buckets.size()); long[] docCounts = new long[buckets.size()]; InternalAggregations[][] aggs = new InternalAggregations[buckets.size()][]; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java index 399dc04e44f5f..a77b70a480850 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java @@ -288,7 +288,7 @@ public B createBucket(InternalAggregations aggregations, B prototype) { @SuppressWarnings("unchecked") @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { reduceContext.consumeBucketsAndMaybeBreak(ranges.size()); List[] rangeList = new List[ranges.size()]; for (int i = 0; i < rangeList.length; ++i) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/UnmappedSampler.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/UnmappedSampler.java index 5f5f557ffd561..6f1a4cc6a4aad 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/UnmappedSampler.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/UnmappedSampler.java @@ -49,7 +49,7 @@ public String getWriteableName() { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { return new UnmappedSampler(name, pipelineAggregators(), metaData); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java index d641a2773e615..47926760d3ba9 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java @@ -126,7 +126,7 @@ public SignificantStringTerms buildAggregation(long owningBucketOrdinal) throws } if (spare == null) { - spare = new SignificantStringTerms.Bucket(new BytesRef(), 0, 0, 0, 0, null, format); + spare = new SignificantStringTerms.Bucket(new BytesRef(), 0, 0, 0, 0, null, format, 0); } spare.bucketOrd = bucketOrd; copy(lookupGlobalOrd.apply(globalOrd), spare.termBytes); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java index 49c2718baaf26..789ced9cd035e 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java @@ -105,6 +105,9 @@ public long getSubsetSize() { return subsetSize; } + // TODO we should refactor to remove this, since buckets should be immutable after they are generated. + // This can lead to confusing bugs if the bucket is re-created (via createBucket() or similar) without + // the score void updateScore(SignificanceHeuristic significanceHeuristic) { score = significanceHeuristic.getScore(subsetDf, subsetSize, supersetDf, supersetSize); } @@ -191,7 +194,7 @@ protected final void doWriteTo(StreamOutput out) throws IOException { public abstract List getBuckets(); @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { long globalSubsetSize = 0; long globalSupersetSize = 0; // Compute the overall result set size and the corpus size using the diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java index 582346f529a8a..d9f4ac7e1f10f 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java @@ -42,14 +42,9 @@ static class Bucket extends InternalSignificantTerms.Bucket { long term; Bucket(long subsetDf, long subsetSize, long supersetDf, long supersetSize, long term, InternalAggregations aggregations, - DocValueFormat format) { + DocValueFormat format, double score) { super(subsetDf, subsetSize, supersetDf, supersetSize, aggregations, format); this.term = term; - } - - Bucket(long subsetDf, long subsetSize, long supersetDf, long supersetSize, long term, InternalAggregations aggregations, - double score) { - this(subsetDf, subsetSize, supersetDf, supersetSize, term, aggregations, null); this.score = score; } @@ -134,7 +129,7 @@ public SignificantLongTerms create(List buckets) { @Override public Bucket createBucket(InternalAggregations aggregations, SignificantLongTerms.Bucket prototype) { return new Bucket(prototype.subsetDf, prototype.subsetSize, prototype.supersetDf, prototype.supersetSize, prototype.term, - aggregations, prototype.format); + aggregations, prototype.format, prototype.score); } @Override @@ -151,6 +146,6 @@ protected Bucket[] createBucketsArray(int size) { @Override Bucket createBucket(long subsetDf, long subsetSize, long supersetDf, long supersetSize, InternalAggregations aggregations, SignificantLongTerms.Bucket prototype) { - return new Bucket(subsetDf, subsetSize, supersetDf, supersetSize, prototype.term, aggregations, format); + return new Bucket(subsetDf, subsetSize, supersetDf, supersetSize, prototype.term, aggregations, format, prototype.score); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java index 2fcba9f09bf7e..8684acb600a2f 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java @@ -88,7 +88,7 @@ public SignificantLongTerms buildAggregation(long owningBucketOrdinal) throws IO continue; } if (spare == null) { - spare = new SignificantLongTerms.Bucket(0, 0, 0, 0, 0, null, format); + spare = new SignificantLongTerms.Bucket(0, 0, 0, 0, 0, null, format, 0); } spare.term = bucketOrds.get(i); spare.subsetDf = docCount; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java index 6c7da48a56092..cb79e52238399 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java @@ -43,9 +43,10 @@ public static class Bucket extends InternalSignificantTerms.Bucket { BytesRef termBytes; public Bucket(BytesRef term, long subsetDf, long subsetSize, long supersetDf, long supersetSize, InternalAggregations aggregations, - DocValueFormat format) { + DocValueFormat format, double score) { super(subsetDf, subsetSize, supersetDf, supersetSize, aggregations, format); this.termBytes = term; + this.score = score; } /** @@ -69,12 +70,6 @@ public void writeTo(StreamOutput out) throws IOException { aggregations.writeTo(out); } - public Bucket(BytesRef term, long subsetDf, long subsetSize, long supersetDf, long supersetSize, - InternalAggregations aggregations, double score, DocValueFormat format) { - this(term, subsetDf, subsetSize, supersetDf, supersetSize, aggregations, format); - this.score = score; - } - @Override public Number getKeyAsNumber() { // this method is needed for scripted numeric aggregations @@ -139,7 +134,7 @@ public SignificantStringTerms create(List buckets @Override public Bucket createBucket(InternalAggregations aggregations, SignificantStringTerms.Bucket prototype) { return new Bucket(prototype.termBytes, prototype.subsetDf, prototype.subsetSize, prototype.supersetDf, prototype.supersetSize, - aggregations, prototype.format); + aggregations, prototype.format, prototype.score); } @Override @@ -156,6 +151,6 @@ protected Bucket[] createBucketsArray(int size) { @Override Bucket createBucket(long subsetDf, long subsetSize, long supersetDf, long supersetSize, InternalAggregations aggregations, SignificantStringTerms.Bucket prototype) { - return new Bucket(prototype.termBytes, subsetDf, subsetSize, supersetDf, supersetSize, aggregations, format); + return new Bucket(prototype.termBytes, subsetDf, subsetSize, supersetDf, supersetSize, aggregations, format, prototype.score); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java index 91ade2e42f740..ed4c96f19add7 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java @@ -91,7 +91,7 @@ public SignificantStringTerms buildAggregation(long owningBucketOrdinal) throws } if (spare == null) { - spare = new SignificantStringTerms.Bucket(new BytesRef(), 0, 0, 0, 0, null, format); + spare = new SignificantStringTerms.Bucket(new BytesRef(), 0, 0, 0, 0, null, format, 0); } bucketOrds.get(i, spare.termBytes); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregator.java index 7f62813278b64..170b0e1fd81c5 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregator.java @@ -201,7 +201,7 @@ public SignificantStringTerms buildAggregation(long owningBucketOrdinal) throws } if (spare == null) { - spare = new SignificantStringTerms.Bucket(new BytesRef(), 0, 0, 0, 0, null, format); + spare = new SignificantStringTerms.Bucket(new BytesRef(), 0, 0, 0, 0, null, format, 0); } bucketOrds.get(i, spare.termBytes); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java index d9e3c85de3a60..3b88fb0d3e460 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java @@ -105,7 +105,7 @@ Bucket createBucket(long subsetDf, long subsetSize, long supersetDf, long supers } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { return new UnmappedSignificantTerms(name, requiredSize, minDocCount, pipelineAggregators(), metaData); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java index 8bc0e83c8d6a6..9672136cf5bea 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java @@ -144,7 +144,7 @@ protected Bucket[] createBucketsArray(int size) { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { boolean promoteToDouble = false; for (InternalAggregation agg : aggregations) { if (agg instanceof LongTerms && ((LongTerms) agg).format == DocValueFormat.RAW) { @@ -157,7 +157,7 @@ public InternalAggregation doReduce(List aggregations, Redu } } if (promoteToDouble == false) { - return super.doReduce(aggregations, reduceContext); + return super.reduce(aggregations, reduceContext); } List newAggs = new ArrayList<>(aggregations.size()); for (InternalAggregation agg : aggregations) { @@ -168,7 +168,7 @@ public InternalAggregation doReduce(List aggregations, Redu newAggs.add(agg); } } - return newAggs.get(0).doReduce(newAggs, reduceContext); + return newAggs.get(0).reduce(newAggs, reduceContext); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalMappedRareTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalMappedRareTerms.java index 7711946226470..bc8e8198984ef 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalMappedRareTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalMappedRareTerms.java @@ -87,7 +87,7 @@ protected void writeTermTypeInfoTo(StreamOutput out) throws IOException { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { Map> buckets = new HashMap<>(); InternalRareTerms referenceTerms = null; SetBackedScalingCuckooFilter filter = null; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalRareTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalRareTerms.java index ae9f8e27ec6ae..dee3424621cd6 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalRareTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalRareTerms.java @@ -154,7 +154,7 @@ protected final void doWriteTo(StreamOutput out) throws IOException { public abstract B getBucketByKey(String term); @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { throw new UnsupportedOperationException(); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java index 3eefc9bee0100..8f45749a36304 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java @@ -192,7 +192,7 @@ protected final void doWriteTo(StreamOutput out) throws IOException { public abstract B getBucketByKey(String term); @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { Map> buckets = new HashMap<>(); long sumDocCountError = 0; long otherDocCount = 0; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java index 6a0fcde1fa053..90ebe7af36abe 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java @@ -144,13 +144,13 @@ protected Bucket[] createBucketsArray(int size) { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { for (InternalAggregation agg : aggregations) { if (agg instanceof DoubleTerms) { - return agg.doReduce(aggregations, reduceContext); + return agg.reduce(aggregations, reduceContext); } } - return super.doReduce(aggregations, reduceContext); + return super.reduce(aggregations, reduceContext); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedRareTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedRareTerms.java index c4a019e6fe9b2..58dac3ece2b24 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedRareTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedRareTerms.java @@ -93,7 +93,7 @@ protected UnmappedRareTerms createWithFilter(String name, List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { return new UnmappedRareTerms(name, pipelineAggregators(), metaData); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java index 8096366f6d655..2a1baff9ded4d 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java @@ -99,7 +99,7 @@ protected UnmappedTerms create(String name, List buckets, long docCountE } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { return new UnmappedTerms(name, order, requiredSize, minDocCount, pipelineAggregators(), metaData); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractInternalHDRPercentiles.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractInternalHDRPercentiles.java index d8e043ee9b5e9..692ea1761fe3c 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractInternalHDRPercentiles.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractInternalHDRPercentiles.java @@ -103,7 +103,7 @@ DoubleHistogram getState() { } @Override - public AbstractInternalHDRPercentiles doReduce(List aggregations, ReduceContext reduceContext) { + public AbstractInternalHDRPercentiles reduce(List aggregations, ReduceContext reduceContext) { DoubleHistogram merged = null; for (InternalAggregation aggregation : aggregations) { final AbstractInternalHDRPercentiles percentiles = (AbstractInternalHDRPercentiles) aggregation; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractInternalTDigestPercentiles.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractInternalTDigestPercentiles.java index ca03e2aa2b1c9..f691a438d0d07 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractInternalTDigestPercentiles.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractInternalTDigestPercentiles.java @@ -87,7 +87,7 @@ TDigestState getState() { } @Override - public AbstractInternalTDigestPercentiles doReduce(List aggregations, ReduceContext reduceContext) { + public AbstractInternalTDigestPercentiles reduce(List aggregations, ReduceContext reduceContext) { TDigestState merged = null; for (InternalAggregation aggregation : aggregations) { final AbstractInternalTDigestPercentiles percentiles = (AbstractInternalTDigestPercentiles) aggregation; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalAvg.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalAvg.java index 3e3b2ae03ea0d..199f3a7ccc872 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalAvg.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalAvg.java @@ -87,7 +87,7 @@ public String getWriteableName() { } @Override - public InternalAvg doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAvg reduce(List aggregations, ReduceContext reduceContext) { CompensatedSum kahanSummation = new CompensatedSum(0, 0); long count = 0; // Compute the sum of double values with Kahan summation algorithm which is more diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalCardinality.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalCardinality.java index c3132a299042e..bc2c4d88c4679 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalCardinality.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalCardinality.java @@ -85,7 +85,7 @@ public HyperLogLogPlusPlus getCounts() { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { InternalCardinality reduced = null; for (InternalAggregation aggregation : aggregations) { final InternalCardinality cardinality = (InternalCardinality) aggregation; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalExtendedStats.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalExtendedStats.java index 3fe2e75576aa4..5385960579451 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalExtendedStats.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalExtendedStats.java @@ -140,7 +140,7 @@ public String getStdDeviationBoundAsString(Bounds bound) { } @Override - public InternalExtendedStats doReduce(List aggregations, ReduceContext reduceContext) { + public InternalExtendedStats reduce(List aggregations, ReduceContext reduceContext) { double sumOfSqrs = 0; double compensationOfSqrs = 0; for (InternalAggregation aggregation : aggregations) { @@ -158,7 +158,7 @@ public InternalExtendedStats doReduce(List aggregations, Re sumOfSqrs = newSumOfSqrs; } } - final InternalStats stats = super.doReduce(aggregations, reduceContext); + final InternalStats stats = super.reduce(aggregations, reduceContext); return new InternalExtendedStats(name, stats.getCount(), stats.getSum(), stats.getMin(), stats.getMax(), sumOfSqrs, sigma, format, pipelineAggregators(), getMetaData()); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java index 4d48e4ab8966b..91007ab2f8fff 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java @@ -93,7 +93,7 @@ public String getWriteableName() { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { double top = Double.NEGATIVE_INFINITY; double bottom = Double.POSITIVE_INFINITY; double posLeft = Double.POSITIVE_INFINITY; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoCentroid.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoCentroid.java index 2172d15259b85..24493273aa534 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoCentroid.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoCentroid.java @@ -114,7 +114,7 @@ public long count() { } @Override - public InternalGeoCentroid doReduce(List aggregations, ReduceContext reduceContext) { + public InternalGeoCentroid reduce(List aggregations, ReduceContext reduceContext) { double lonSum = Double.NaN; double latSum = Double.NaN; int totalCount = 0; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMax.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMax.java index 9a8458c85a690..6abb0d3a51dd8 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMax.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMax.java @@ -71,7 +71,7 @@ public double getValue() { } @Override - public InternalMax doReduce(List aggregations, ReduceContext reduceContext) { + public InternalMax reduce(List aggregations, ReduceContext reduceContext) { double max = Double.NEGATIVE_INFINITY; for (InternalAggregation aggregation : aggregations) { max = Math.max(max, ((InternalMax) aggregation).max); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMedianAbsoluteDeviation.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMedianAbsoluteDeviation.java index 871f387638dc3..b228c95c0dc3b 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMedianAbsoluteDeviation.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMedianAbsoluteDeviation.java @@ -80,7 +80,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { final TDigestState valueMerged = new TDigestState(valuesSketch.compression()); for (InternalAggregation aggregation : aggregations) { final InternalMedianAbsoluteDeviation madAggregation = (InternalMedianAbsoluteDeviation) aggregation; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMin.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMin.java index f68d5a46860bc..6912f55ecafdc 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMin.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMin.java @@ -71,7 +71,7 @@ public double getValue() { } @Override - public InternalMin doReduce(List aggregations, ReduceContext reduceContext) { + public InternalMin reduce(List aggregations, ReduceContext reduceContext) { double min = Double.POSITIVE_INFINITY; for (InternalAggregation aggregation : aggregations) { min = Math.min(min, ((InternalMin) aggregation).min); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalScriptedMetric.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalScriptedMetric.java index ce051a1691b55..e6113f3763cdf 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalScriptedMetric.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalScriptedMetric.java @@ -85,7 +85,7 @@ List getAggregation() { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { List aggregationObjects = new ArrayList<>(); for (InternalAggregation aggregation : aggregations) { InternalScriptedMetric mapReduceAggregation = (InternalScriptedMetric) aggregation; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalStats.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalStats.java index adb879999a410..661457e1c1f3d 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalStats.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalStats.java @@ -145,7 +145,7 @@ public double value(String name) { } @Override - public InternalStats doReduce(List aggregations, ReduceContext reduceContext) { + public InternalStats reduce(List aggregations, ReduceContext reduceContext) { long count = 0; double min = Double.POSITIVE_INFINITY; double max = Double.NEGATIVE_INFINITY; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalSum.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalSum.java index 6e6315eded101..5778edb4da19a 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalSum.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalSum.java @@ -71,7 +71,7 @@ public double getValue() { } @Override - public InternalSum doReduce(List aggregations, ReduceContext reduceContext) { + public InternalSum reduce(List aggregations, ReduceContext reduceContext) { // Compute the sum of double values with Kahan summation algorithm which is more // accurate than naive summation. CompensatedSum kahanSummation = new CompensatedSum(0, 0); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalTopHits.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalTopHits.java index 348e98302d2bc..4f922bd39d947 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalTopHits.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalTopHits.java @@ -99,7 +99,7 @@ int getSize() { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { final SearchHits[] shardHits = new SearchHits[aggregations.size()]; final int from; final int size; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalValueCount.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalValueCount.java index 32ee8bd36d120..0c942e1afbcaf 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalValueCount.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalValueCount.java @@ -70,7 +70,7 @@ public double value() { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { long valueCount = 0; for (InternalAggregation aggregation : aggregations) { valueCount += ((InternalValueCount) aggregation).value; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalWeightedAvg.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalWeightedAvg.java index 4b3523b03ac3d..e4c79f7f8996f 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalWeightedAvg.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalWeightedAvg.java @@ -87,7 +87,7 @@ public String getWriteableName() { } @Override - public InternalWeightedAvg doReduce(List aggregations, ReduceContext reduceContext) { + public InternalWeightedAvg reduce(List aggregations, ReduceContext reduceContext) { CompensatedSum sumCompensation = new CompensatedSum(0, 0); CompensatedSum weightCompensation = new CompensatedSum(0, 0); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalBucketMetricValue.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalBucketMetricValue.java index 1acdb54080693..84b4c3b305a41 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalBucketMetricValue.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalBucketMetricValue.java @@ -85,7 +85,7 @@ DocValueFormat formatter() { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { throw new UnsupportedOperationException("Not supported"); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalExtendedStatsBucket.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalExtendedStatsBucket.java index b0b78eb012042..d1f60fe30e511 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalExtendedStatsBucket.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalExtendedStatsBucket.java @@ -48,7 +48,7 @@ public String getWriteableName() { } @Override - public InternalExtendedStats doReduce(List aggregations, ReduceContext reduceContext) { + public InternalExtendedStats reduce(List aggregations, ReduceContext reduceContext) { throw new UnsupportedOperationException("Not supported"); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucket.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucket.java index 77493f66d643e..38eaae87c012f 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucket.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucket.java @@ -119,7 +119,7 @@ public double value(String name) { } @Override - public InternalMax doReduce(List aggregations, ReduceContext reduceContext) { + public InternalMax reduce(List aggregations, ReduceContext reduceContext) { throw new UnsupportedOperationException("Not supported"); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalSimpleValue.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalSimpleValue.java index 4f7b51b6e3b38..29c25b117467f 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalSimpleValue.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalSimpleValue.java @@ -76,7 +76,7 @@ DocValueFormat formatter() { } @Override - public InternalSimpleValue doReduce(List aggregations, ReduceContext reduceContext) { + public InternalSimpleValue reduce(List aggregations, ReduceContext reduceContext) { throw new UnsupportedOperationException("Not supported"); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalStatsBucket.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalStatsBucket.java index 51d3cfc060f73..c8beef459b8c8 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalStatsBucket.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalStatsBucket.java @@ -47,7 +47,7 @@ public String getWriteableName() { } @Override - public InternalStats doReduce(List aggregations, ReduceContext reduceContext) { + public InternalStats reduce(List aggregations, ReduceContext reduceContext) { throw new UnsupportedOperationException("Not supported"); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationsTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationsTests.java index cf6a9f6d838ee..7236b354ef92b 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationsTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationsTests.java @@ -62,7 +62,7 @@ public void testNonFinalReduceTopLevelPipelineAggs() { List aggs = Collections.singletonList(new InternalAggregations(Collections.singletonList(terms), topLevelPipelineAggs)); InternalAggregation.ReduceContext reduceContext = new InternalAggregation.ReduceContext(null, null, false); - InternalAggregations reducedAggs = InternalAggregations.reduce(aggs, reduceContext); + InternalAggregations reducedAggs = InternalAggregations.topLevelReduce(aggs, reduceContext); assertEquals(1, reducedAggs.getTopLevelPipelineAggregators().size()); assertEquals(1, reducedAggs.aggregations.size()); } @@ -78,11 +78,11 @@ public void testFinalReduceTopLevelPipelineAggs() { if (randomBoolean()) { InternalAggregations aggs = new InternalAggregations(Collections.singletonList(terms), Collections.singletonList(siblingPipelineAggregator)); - reducedAggs = InternalAggregations.reduce(Collections.singletonList(aggs), reduceContext); + reducedAggs = InternalAggregations.topLevelReduce(Collections.singletonList(aggs), reduceContext); } else { InternalAggregations aggs = new InternalAggregations(Collections.singletonList(terms), Collections.singletonList(siblingPipelineAggregator)); - reducedAggs = InternalAggregations.reduce(Collections.singletonList(aggs), reduceContext); + reducedAggs = InternalAggregations.topLevelReduce(Collections.singletonList(aggs), reduceContext); } assertEquals(0, reducedAggs.getTopLevelPipelineAggregators().size()); assertEquals(2, reducedAggs.aggregations.size()); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/AutoDateHistogramAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/AutoDateHistogramAggregatorTests.java index 9293b33e22f43..c6bb10fa6c947 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/AutoDateHistogramAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/AutoDateHistogramAggregatorTests.java @@ -36,10 +36,15 @@ import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DateFieldMapper; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.NumberFieldMapper; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.AggregatorTestCase; import org.elasticsearch.search.aggregations.MultiBucketConsumerService; +import org.elasticsearch.search.aggregations.metrics.InternalMax; import org.elasticsearch.search.aggregations.metrics.InternalStats; +import org.elasticsearch.search.aggregations.pipeline.DerivativePipelineAggregationBuilder; +import org.elasticsearch.search.aggregations.pipeline.InternalSimpleValue; import org.elasticsearch.search.aggregations.support.AggregationInspectionHelper; import org.hamcrest.Matchers; import org.junit.Assert; @@ -58,9 +63,12 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import static org.hamcrest.Matchers.equalTo; + public class AutoDateHistogramAggregatorTests extends AggregatorTestCase { private static final String DATE_FIELD = "date"; private static final String INSTANT_FIELD = "instant"; + private static final String NUMERIC_FIELD = "numeric"; private static final List DATES_WITH_TIME = Arrays.asList( ZonedDateTime.of(2010, 3, 12, 1, 7, 45, 0, ZoneOffset.UTC), @@ -718,6 +726,35 @@ public void testIntervalSecond() throws IOException { ); } + public void testWithPipelineReductions() throws IOException { + testSearchAndReduceCase(DEFAULT_QUERY, DATES_WITH_TIME, + aggregation -> aggregation.setNumBuckets(1).field(DATE_FIELD) + .subAggregation(AggregationBuilders.histogram("histo").field(NUMERIC_FIELD).interval(1) + .subAggregation(AggregationBuilders.max("max").field(NUMERIC_FIELD)) + .subAggregation(new DerivativePipelineAggregationBuilder("deriv", "max"))), + histogram -> { + assertTrue(AggregationInspectionHelper.hasValue(histogram)); + final List buckets = histogram.getBuckets(); + assertEquals(1, buckets.size()); + + Histogram.Bucket bucket = buckets.get(0); + assertEquals("2010-01-01T00:00:00.000Z", bucket.getKeyAsString()); + assertEquals(10, bucket.getDocCount()); + assertThat(bucket.getAggregations().asList().size(), equalTo(1)); + InternalHistogram histo = (InternalHistogram) bucket.getAggregations().asList().get(0); + assertThat(histo.getBuckets().size(), equalTo(10)); + for (int i = 0; i < 10; i++) { + assertThat(histo.getBuckets().get(i).key, equalTo((double)i)); + assertThat(((InternalMax)histo.getBuckets().get(i).aggregations.get("max")).getValue(), equalTo((double)i)); + if (i > 0) { + assertThat(((InternalSimpleValue)histo.getBuckets().get(i).aggregations.get("deriv")).getValue(), equalTo(1.0)); + } + } + + + }); + } + private void testSearchCase(final Query query, final List dataset, final Consumer configure, final Consumer verify) throws IOException { @@ -757,6 +794,7 @@ private void executeTestCase(final boolean reduced, final Query query, final Lis try (Directory directory = newDirectory()) { try (RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory)) { final Document document = new Document(); + int i = 0; for (final ZonedDateTime date : dataset) { if (frequently()) { indexWriter.commit(); @@ -765,8 +803,10 @@ private void executeTestCase(final boolean reduced, final Query query, final Lis final long instant = date.toInstant().toEpochMilli(); document.add(new SortedNumericDocValuesField(DATE_FIELD, instant)); document.add(new LongPoint(INSTANT_FIELD, instant)); + document.add(new SortedNumericDocValuesField(NUMERIC_FIELD, i)); indexWriter.addDocument(document); document.clear(); + i += 1; } } @@ -783,11 +823,19 @@ private void executeTestCase(final boolean reduced, final Query query, final Lis fieldType.setHasDocValues(true); fieldType.setName(aggregationBuilder.field()); + MappedFieldType instantFieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG); + instantFieldType.setName(INSTANT_FIELD); + instantFieldType.setHasDocValues(true); + + MappedFieldType numericFieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG); + numericFieldType.setName(NUMERIC_FIELD); + numericFieldType.setHasDocValues(true); + final InternalAutoDateHistogram histogram; if (reduced) { - histogram = searchAndReduce(indexSearcher, query, aggregationBuilder, fieldType); + histogram = searchAndReduce(indexSearcher, query, aggregationBuilder, fieldType, instantFieldType, numericFieldType); } else { - histogram = search(indexSearcher, query, aggregationBuilder, fieldType); + histogram = search(indexSearcher, query, aggregationBuilder, fieldType, instantFieldType, numericFieldType); } verify.accept(histogram); } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogramTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogramTests.java index fa4bce9a4e959..4ada5e349de1f 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogramTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogramTests.java @@ -108,7 +108,6 @@ public void testGetAppropriateRoundingUsesCorrectIntervals() { assertThat(result, equalTo(2)); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/39497") public void testReduceRandom() { super.testReduceRandom(); } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java index fb9f6dd29c73f..5b8264b9e71ea 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java @@ -109,7 +109,7 @@ public void testHandlesNaN() { newBuckets.add(new InternalHistogram.Bucket(Double.NaN, b.docCount, keyed, b.format, b.aggregations)); InternalHistogram newHistogram = histogram.create(newBuckets); - newHistogram.doReduce(Arrays.asList(newHistogram, histogram2), new InternalAggregation.ReduceContext(null, null, false)); + newHistogram.reduce(Arrays.asList(newHistogram, histogram2), new InternalAggregation.ReduceContext(null, null, false)); } @Override diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java index 49de4eb821115..6afc5e94e3029 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java @@ -126,12 +126,12 @@ public void testStreamResponse() throws Exception { InternalMappedSignificantTerms getRandomSignificantTerms(SignificanceHeuristic heuristic) { if (randomBoolean()) { SignificantLongTerms.Bucket bucket = new SignificantLongTerms.Bucket(1, 2, 3, 4, 123, InternalAggregations.EMPTY, - DocValueFormat.RAW); + DocValueFormat.RAW, randomDoubleBetween(0, 100, true)); return new SignificantLongTerms("some_name", 1, 1, emptyList(), null, DocValueFormat.RAW, 10, 20, heuristic, singletonList(bucket)); } else { SignificantStringTerms.Bucket bucket = new SignificantStringTerms.Bucket(new BytesRef("someterm"), 1, 2, 3, 4, - InternalAggregations.EMPTY, DocValueFormat.RAW); + InternalAggregations.EMPTY, DocValueFormat.RAW, randomDoubleBetween(0, 100, true)); return new SignificantStringTerms("some_name", 1, 1, emptyList(), null, DocValueFormat.RAW, 10, 20, heuristic, singletonList(bucket)); } @@ -149,7 +149,7 @@ public static SignificanceHeuristic getRandomSignificanceheuristic() { public void testReduce() { List aggs = createInternalAggregations(); InternalAggregation.ReduceContext context = new InternalAggregation.ReduceContext(null, null, true); - SignificantTerms reducedAgg = (SignificantTerms) aggs.get(0).doReduce(aggs, context); + SignificantTerms reducedAgg = (SignificantTerms) aggs.get(0).reduce(aggs, context); assertThat(reducedAgg.getBuckets().size(), equalTo(2)); assertThat(reducedAgg.getBuckets().get(0).getSubsetDf(), equalTo(8L)); assertThat(reducedAgg.getBuckets().get(0).getSubsetSize(), equalTo(16L)); @@ -196,7 +196,7 @@ SignificantStringTerms createAggregation(SignificanceHeuristic significanceHeuri @Override SignificantStringTerms.Bucket createBucket(long subsetDF, long subsetSize, long supersetDF, long supersetSize, long label) { return new SignificantStringTerms.Bucket(new BytesRef(Long.toString(label).getBytes(StandardCharsets.UTF_8)), subsetDF, - subsetSize, supersetDF, supersetSize, InternalAggregations.EMPTY, DocValueFormat.RAW); + subsetSize, supersetDF, supersetSize, InternalAggregations.EMPTY, DocValueFormat.RAW, 0); } } private class LongTestAggFactory extends TestAggFactory { @@ -210,7 +210,7 @@ SignificantLongTerms createAggregation(SignificanceHeuristic significanceHeurist @Override SignificantLongTerms.Bucket createBucket(long subsetDF, long subsetSize, long supersetDF, long supersetSize, long label) { return new SignificantLongTerms.Bucket(subsetDF, subsetSize, supersetDF, supersetSize, label, InternalAggregations.EMPTY, - DocValueFormat.RAW); + DocValueFormat.RAW, 0); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsTests.java index 755cb6e85292d..3a9684d305114 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsTests.java @@ -58,7 +58,7 @@ protected InternalSignificantTerms createTestInstance(String name, for (int i = 0; i < numBuckets; ++i) { long term = randomValueOtherThanMany(l -> terms.add(l) == false, random()::nextLong); SignificantLongTerms.Bucket bucket = new SignificantLongTerms.Bucket(subsetDfs[i], subsetSize, - supersetDfs[i], supersetSize, term, aggs, format); + supersetDfs[i], supersetSize, term, aggs, format, 0); bucket.updateScore(significanceHeuristic); buckets.add(bucket); } @@ -109,7 +109,7 @@ protected Class implementationClass() { case 5: buckets = new ArrayList<>(buckets); buckets.add(new SignificantLongTerms.Bucket(randomLong(), randomNonNegativeLong(), randomNonNegativeLong(), - randomNonNegativeLong(), randomNonNegativeLong(), InternalAggregations.EMPTY, format)); + randomNonNegativeLong(), randomNonNegativeLong(), InternalAggregations.EMPTY, format, 0)); break; case 8: if (metaData == null) { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsTests.java index 2255373fd346d..d230b681cbe68 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsTests.java @@ -51,7 +51,7 @@ protected InternalSignificantTerms createTestInstance(String name, for (int i = 0; i < numBuckets; ++i) { BytesRef term = randomValueOtherThanMany(b -> terms.add(b) == false, () -> new BytesRef(randomAlphaOfLength(10))); SignificantStringTerms.Bucket bucket = new SignificantStringTerms.Bucket(term, subsetDfs[i], subsetSize, - supersetDfs[i], supersetSize, aggs, format); + supersetDfs[i], supersetSize, aggs, format, 0); bucket.updateScore(significanceHeuristic); buckets.add(bucket); } @@ -103,7 +103,7 @@ protected Class implementationClass() { buckets = new ArrayList<>(buckets); buckets.add(new SignificantStringTerms.Bucket(new BytesRef(randomAlphaOfLengthBetween(1, 10)), randomNonNegativeLong(), randomNonNegativeLong(), randomNonNegativeLong(), randomNonNegativeLong(), - InternalAggregations.EMPTY, format)); + InternalAggregations.EMPTY, format, 0)); break; case 8: if (metaData == null) { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorTests.java index 727c3ea3a87ae..611e7d916c9c9 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorTests.java @@ -1073,7 +1073,7 @@ public void testMixLongAndDouble() throws Exception { new InternalAggregation.ReduceContext(new MockBigArrays(new MockPageCacheRecycler(Settings.EMPTY), new NoneCircuitBreakerService()), null, true); for (InternalAggregation internalAgg : aggs) { - InternalAggregation mergedAggs = internalAgg.doReduce(aggs, ctx); + InternalAggregation mergedAggs = internalAgg.reduce(aggs, ctx); assertTrue(mergedAggs instanceof DoubleTerms); long expected = numLongs + numDoubles; List buckets = ((DoubleTerms) mergedAggs).getBuckets(); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalAvgTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalAvgTests.java index 10ae10a9af1c0..5582af4ced64f 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalAvgTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalAvgTests.java @@ -23,8 +23,6 @@ import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.ParsedAggregation; -import org.elasticsearch.search.aggregations.metrics.InternalAvg; -import org.elasticsearch.search.aggregations.metrics.ParsedAvg; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.elasticsearch.test.InternalAggregationTestCase; @@ -95,7 +93,7 @@ private void verifyAvgOfDoubles(double[] values, double expected, double delta) aggregations.add(new InternalAvg("dummy1", value, 1, null, null, null)); } InternalAvg internalAvg = new InternalAvg("dummy2", 0, 0, null, null, null); - InternalAvg reduced = internalAvg.doReduce(aggregations, null); + InternalAvg reduced = internalAvg.reduce(aggregations, null); assertEquals(expected, reduced.getValue(), delta); } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalExtendedStatsTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalExtendedStatsTests.java index 3c5201bfa8aa9..2f53902bb537a 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalExtendedStatsTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalExtendedStatsTests.java @@ -225,7 +225,7 @@ private void verifySumOfSqrsOfDoubles(double[] values, double expectedSumOfSqrs, aggregations.add(new InternalExtendedStats("dummy1", 1, 0.0, 0.0, 0.0, sumOfSqrs, sigma, null, null, null)); } InternalExtendedStats stats = new InternalExtendedStats("dummy", 1, 0.0, 0.0, 0.0, 0.0, sigma, null, null, null); - InternalExtendedStats reduced = stats.doReduce(aggregations, null); + InternalExtendedStats reduced = stats.reduce(aggregations, null); assertEquals(expectedSumOfSqrs, reduced.getSumOfSquares(), delta); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalStatsTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalStatsTests.java index 8198d6c2e81a3..e4a56c0c5b84a 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalStatsTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalStatsTests.java @@ -114,7 +114,7 @@ private void verifyStatsOfDoubles(double[] values, double expectedSum, double ex aggregations.add(new InternalStats("dummy1", 1, value, value, value, null, null, null)); } InternalStats internalStats = new InternalStats("dummy2", 0, 0.0, 2.0, 0.0, null, null, null); - InternalStats reduced = internalStats.doReduce(aggregations, null); + InternalStats reduced = internalStats.reduce(aggregations, null); assertEquals("dummy2", reduced.getName()); assertEquals(values.length, reduced.getCount()); assertEquals(expectedSum, reduced.getSum(), delta); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalSumTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalSumTests.java index 4f44be7d50833..0fca0d43bd6f1 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalSumTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalSumTests.java @@ -87,7 +87,7 @@ private void verifySummationOfDoubles(double[] values, double expected, double d aggregations.add(new InternalSum("dummy1", value, null, null, null)); } InternalSum internalSum = new InternalSum("dummy", 0, null, null, null); - InternalSum reduced = internalSum.doReduce(aggregations, null); + InternalSum reduced = internalSum.reduce(aggregations, null); assertEquals(expected, reduced.value(), delta); } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/AvgBucketAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/AvgBucketAggregatorTests.java index afea0f13bd7db..b8e9f1444dd6a 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/AvgBucketAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/AvgBucketAggregatorTests.java @@ -77,7 +77,7 @@ public class AvgBucketAggregatorTests extends AggregatorTestCase { * it is fixed. * * Note: we have this test inside of the `avg_bucket` package so that we can get access to the package-private - * `doReduce()` needed for testing this + * `reduce()` needed for testing this */ public void testSameAggNames() throws IOException { Query query = new MatchAllDocsQuery(); diff --git a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java index 199f9b055393c..221030cd8d157 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java @@ -387,7 +387,7 @@ protected A searchAndReduc InternalAggregation.ReduceContext context = new InternalAggregation.ReduceContext(root.context().bigArrays(), getMockScriptService(), reduceBucketConsumer, false); - A reduced = (A) aggs.get(0).doReduce(toReduce, context); + A reduced = (A) aggs.get(0).reduce(toReduce, context); doAssertReducedMultiBucketConsumer(reduced, reduceBucketConsumer); aggs = new ArrayList<>(aggs.subList(r, toReduceSize)); aggs.add(reduced); @@ -398,7 +398,12 @@ protected A searchAndReduc new InternalAggregation.ReduceContext(root.context().bigArrays(), getMockScriptService(), reduceBucketConsumer, true); @SuppressWarnings("unchecked") - A internalAgg = (A) aggs.get(0).doReduce(aggs, context); + A internalAgg = (A) aggs.get(0).reduce(aggs, context); + + // materialize any parent pipelines + internalAgg = (A) internalAgg.reducePipelines(internalAgg, context); + + // materialize any sibling pipelines at top level if (internalAgg.pipelineAggregators().size() > 0) { for (PipelineAggregator pipelineAggregator : internalAgg.pipelineAggregators()) { internalAgg = (A) pipelineAggregator.reduce(internalAgg, context); diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/cumulativecardinality/InternalSimpleLongValue.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/cumulativecardinality/InternalSimpleLongValue.java index e8db75edad5d9..a6b8aad699824 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/cumulativecardinality/InternalSimpleLongValue.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/cumulativecardinality/InternalSimpleLongValue.java @@ -64,7 +64,7 @@ DocValueFormat formatter() { } @Override - public InternalSimpleLongValue doReduce(List aggregations, ReduceContext reduceContext) { + public InternalSimpleLongValue reduce(List aggregations, ReduceContext reduceContext) { throw new UnsupportedOperationException("Not supported"); } diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStats.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStats.java index 88cb505615cf7..007fbcea9024a 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStats.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStats.java @@ -182,7 +182,7 @@ public Object value(String name) { } @Override - public InternalStringStats doReduce(List aggregations, ReduceContext reduceContext) { + public InternalStringStats reduce(List aggregations, ReduceContext reduceContext) { long count = 0; long totalLength = 0; int minLength = Integer.MAX_VALUE; diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java index 25fe2f51b2f22..91953295bd523 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java @@ -610,7 +610,7 @@ public void testDateHistoWithGap() throws IOException { ScriptService scriptService = mock(ScriptService.class); InternalAggregation.ReduceContext context = new InternalAggregation.ReduceContext(bigArrays, scriptService, true); - InternalAggregation reduced = ((InternalDateHistogram)unrolled).doReduce(Collections.singletonList(unrolled), context); + InternalAggregation reduced = ((InternalDateHistogram)unrolled).reduce(Collections.singletonList(unrolled), context); assertThat(reduced.toString(), equalTo("{\"histo\":{\"buckets\":[{\"key_as_string\":\"1970-01-01T00:00:00.100Z\",\"key\":100," + "\"doc_count\":1},{\"key_as_string\":\"1970-01-01T00:00:00.200Z\",\"key\":200,\"doc_count\":1}," + "{\"key_as_string\":\"1970-01-01T00:00:00.300Z\",\"key\":300,\"doc_count\":0,\"histo._count\":{\"value\":0.0}}," + diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/TestMultiValueAggregation.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/TestMultiValueAggregation.java index 7a24a5515b527..8061ce31ef832 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/TestMultiValueAggregation.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/TestMultiValueAggregation.java @@ -42,7 +42,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { throw new UnsupportedOperationException(); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/TestSingleValueAggregation.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/TestSingleValueAggregation.java index 08f4edda3cbf6..56183414aa0ac 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/TestSingleValueAggregation.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/TestSingleValueAggregation.java @@ -37,7 +37,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { } @Override - public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { throw new UnsupportedOperationException(); } From 65114f7b6e6c335a3504cc6163e3e538534ff43c Mon Sep 17 00:00:00 2001 From: Orhan Toy Date: Thu, 5 Dec 2019 22:33:36 +0100 Subject: [PATCH 085/686] Consistent case in CLI option descriptions (#49635) This commit improves the casing of messages in the CLI help descriptions. --- libs/cli/src/main/java/org/elasticsearch/cli/Command.java | 6 +++--- .../java/org/elasticsearch/bootstrap/Elasticsearch.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/cli/src/main/java/org/elasticsearch/cli/Command.java b/libs/cli/src/main/java/org/elasticsearch/cli/Command.java index 2a270153f474c..9ce77604a5014 100644 --- a/libs/cli/src/main/java/org/elasticsearch/cli/Command.java +++ b/libs/cli/src/main/java/org/elasticsearch/cli/Command.java @@ -43,10 +43,10 @@ public abstract class Command implements Closeable { /** The option parser for this command. */ protected final OptionParser parser = new OptionParser(); - private final OptionSpec helpOption = parser.acceptsAll(Arrays.asList("h", "help"), "show help").forHelp(); - private final OptionSpec silentOption = parser.acceptsAll(Arrays.asList("s", "silent"), "show minimal output"); + private final OptionSpec helpOption = parser.acceptsAll(Arrays.asList("h", "help"), "Show help").forHelp(); + private final OptionSpec silentOption = parser.acceptsAll(Arrays.asList("s", "silent"), "Show minimal output"); private final OptionSpec verboseOption = - parser.acceptsAll(Arrays.asList("v", "verbose"), "show verbose output").availableUnless(silentOption); + parser.acceptsAll(Arrays.asList("v", "verbose"), "Show verbose output").availableUnless(silentOption); /** * Construct the command with the specified command description and runnable to execute before main is invoked. diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java index 3513a967763f5..83cdf533e03a7 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java @@ -52,9 +52,9 @@ class Elasticsearch extends EnvironmentAwareCommand { // visible for testing Elasticsearch() { - super("starts elasticsearch", () -> {}); // we configure logging later so we override the base class from configuring logging + super("Starts Elasticsearch", () -> {}); // we configure logging later so we override the base class from configuring logging versionOption = parser.acceptsAll(Arrays.asList("V", "version"), - "Prints elasticsearch version information and exits"); + "Prints Elasticsearch version information and exits"); daemonizeOption = parser.acceptsAll(Arrays.asList("d", "daemonize"), "Starts Elasticsearch in the background") .availableUnless(versionOption); From 47435981f56aa32b813d0d9cf2714458e309a792 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Thu, 5 Dec 2019 16:31:07 -0800 Subject: [PATCH 086/686] Move BuildParams class to 'minimumRuntime' source set (#49890) Move BuildParams class to 'minimumRuntime' source set to retain compatibility with build-tools for builds using a Java 8 runtime. Closes #49766 --- .../java/org/elasticsearch/gradle/info/BuildParams.java | 4 +++- .../java/org/elasticsearch/gradle/info/JavaHome.java | 0 2 files changed, 3 insertions(+), 1 deletion(-) rename buildSrc/src/{main => minimumRuntime}/java/org/elasticsearch/gradle/info/BuildParams.java (97%) rename buildSrc/src/{main => minimumRuntime}/java/org/elasticsearch/gradle/info/JavaHome.java (100%) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/info/BuildParams.java b/buildSrc/src/minimumRuntime/java/org/elasticsearch/gradle/info/BuildParams.java similarity index 97% rename from buildSrc/src/main/java/org/elasticsearch/gradle/info/BuildParams.java rename to buildSrc/src/minimumRuntime/java/org/elasticsearch/gradle/info/BuildParams.java index 6400e4975ad51..9fba20c7db77c 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/info/BuildParams.java +++ b/buildSrc/src/minimumRuntime/java/org/elasticsearch/gradle/info/BuildParams.java @@ -84,7 +84,6 @@ public static JavaVersion getRuntimeJavaVersion() { return value(runtimeJavaVersion); } - @ExecutionTime public static Boolean isInFipsJvm() { return value(inFipsJvm); } @@ -155,6 +154,9 @@ public void reset() { .filter(f -> Modifier.isStatic(f.getModifiers())) .forEach(f -> { try { + // Since we are mutating private static fields from a public static inner class we need to suppress + // accessibility controls here. + f.setAccessible(true); f.set(null, null); } catch (IllegalAccessException e) { throw new RuntimeException(e); diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/info/JavaHome.java b/buildSrc/src/minimumRuntime/java/org/elasticsearch/gradle/info/JavaHome.java similarity index 100% rename from buildSrc/src/main/java/org/elasticsearch/gradle/info/JavaHome.java rename to buildSrc/src/minimumRuntime/java/org/elasticsearch/gradle/info/JavaHome.java From 38f7d8b3b58b73ea36ded800f1c4888b04be1d4d Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Thu, 5 Dec 2019 16:57:45 -0800 Subject: [PATCH 087/686] Add nodes to handle types (#49785) This PR adds 3 nodes to handle types defined by a front-end creating a Painless AST. These types are decided with data immutability in mind - hence the reason for more than a single node. --- .../elasticsearch/painless/antlr/Walker.java | 4 +- .../painless/node/DResolvedType.java | 81 ++++++++++++++ .../elasticsearch/painless/node/DType.java | 46 ++++++++ .../painless/node/DUnresolvedType.java | 61 +++++++++++ .../painless/node/SDeclaration.java | 14 +-- .../painless/node/NodeToStringTests.java | 101 +++++++++--------- 6 files changed, 247 insertions(+), 60 deletions(-) create mode 100644 modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DResolvedType.java create mode 100644 modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DType.java create mode 100644 modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DUnresolvedType.java diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java index 84f44c832d365..53c98f7589ef3 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java @@ -109,6 +109,7 @@ import org.elasticsearch.painless.node.AExpression; import org.elasticsearch.painless.node.ANode; import org.elasticsearch.painless.node.AStatement; +import org.elasticsearch.painless.node.DUnresolvedType; import org.elasticsearch.painless.node.EAssignment; import org.elasticsearch.painless.node.EBinary; import org.elasticsearch.painless.node.EBool; @@ -478,8 +479,9 @@ public ANode visitDeclaration(DeclarationContext ctx) { for (DeclvarContext declvar : ctx.declvar()) { String name = declvar.ID().getText(); AExpression expression = declvar.expression() == null ? null : (AExpression)visit(declvar.expression()); + DUnresolvedType unresolvedType = new DUnresolvedType(location(declvar), type); - declarations.add(new SDeclaration(location(declvar), type, name, expression)); + declarations.add(new SDeclaration(location(declvar), unresolvedType, name, expression)); } return new SDeclBlock(location(ctx), declarations); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DResolvedType.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DResolvedType.java new file mode 100644 index 0000000000000..223b39068673d --- /dev/null +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DResolvedType.java @@ -0,0 +1,81 @@ +/* + * 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. + */ + +package org.elasticsearch.painless.node; + +import org.elasticsearch.painless.Location; +import org.elasticsearch.painless.lookup.PainlessLookup; +import org.elasticsearch.painless.lookup.PainlessLookupUtility; + +import java.util.Objects; + +/** + * Represents a Painless type as a {@link Class}. This may still + * require resolution to ensure the type in the {@link PainlessLookup}. + */ +public class DResolvedType extends DType { + + protected final Class type; + + /** + * If set to {@code true} ensures the type is in the {@link PainlessLookup}. + * If set to {@code false} assumes the type is valid. + */ + protected final boolean checkInLookup; + + public DResolvedType(Location location, Class type) { + this(location, type, true); + } + + public DResolvedType(Location location, Class type, boolean checkInLookup) { + super(location); + this.type = Objects.requireNonNull(type); + this.checkInLookup = checkInLookup; + } + + /** + * If {@link #checkInLookup} is {@code true} checks if the type is in the + * {@link PainlessLookup}, otherwise returns {@code this}. + * @throws IllegalArgumentException if both checking the type is in the {@link PainlessLookup} + * and the type cannot be resolved from the {@link PainlessLookup} + * @return a {@link DResolvedType} where the resolved Painless type is retrievable + */ + @Override + public DResolvedType resolveType(PainlessLookup painlessLookup) { + if (checkInLookup == false) { + return this; + } + + if (painlessLookup.getClasses().contains(type) == false) { + throw location.createError(new IllegalArgumentException( + "cannot resolve type [" + PainlessLookupUtility.typeToCanonicalTypeName(type) + "]")); + } + + return new DResolvedType(location, type, false); + } + + public Class getType() { + return type; + } + + @Override + public String toString() { + return " (DResolvedType [" + PainlessLookupUtility.typeToCanonicalTypeName(type) + "])"; + } +} diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DType.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DType.java new file mode 100644 index 0000000000000..5470739170651 --- /dev/null +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DType.java @@ -0,0 +1,46 @@ +/* + * 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. + */ + +package org.elasticsearch.painless.node; + +import org.elasticsearch.painless.Location; +import org.elasticsearch.painless.lookup.PainlessLookup; + +import java.util.Objects; + +/** + * Represents an abstract Painless type. {@link DType} nodes must be + * resolved using {@link #resolveType(PainlessLookup)} to retrieve the + * actual Painless type. {@link DType} exists as a base class so consumers + * may have either a {@link DUnresolvedType} representing a Painless + * canonical type name or a {@link DResolvedType} representing a Painless + * type as the Painless AST is constructed. This allows Painless types already + * resolved at the time of Painless AST construction to not be forced to + * convert back to a Painless canonical type name and then re-resolved. + */ +public abstract class DType { + + protected final Location location; + + public DType(Location location) { + this.location = Objects.requireNonNull(location); + } + + public abstract DResolvedType resolveType(PainlessLookup painlessLookup); +} diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DUnresolvedType.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DUnresolvedType.java new file mode 100644 index 0000000000000..8e21507fdaa05 --- /dev/null +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DUnresolvedType.java @@ -0,0 +1,61 @@ +/* + * 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. + */ + +package org.elasticsearch.painless.node; + +import org.elasticsearch.painless.Location; +import org.elasticsearch.painless.lookup.PainlessLookup; + +import java.util.Objects; + +/** + * Represents a canonical Painless type name as a {@link String} + * that requires resolution. + */ +public class DUnresolvedType extends DType { + + protected final String typeName; + + public DUnresolvedType(Location location, String typeName) { + super(location); + this.typeName = Objects.requireNonNull(typeName); + } + + /** + * Resolves the canonical Painless type name to a Painless type. + * @throws IllegalArgumentException if the type cannot be resolved from the {@link PainlessLookup} + * @return a {@link DResolvedType} where the resolved Painless type is retrievable + */ + @Override + public DResolvedType resolveType(PainlessLookup painlessLookup) { + Class type = painlessLookup.canonicalTypeNameToType(typeName); + + if (type == null) { + throw location.createError(new IllegalArgumentException("cannot resolve type [" + typeName + "]")); + } + + return new DResolvedType(location, type); + } + + @Override + public String toString() { + return "(DUnresolvedType [" + typeName + "])"; + } +} + diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java index de42c43db4ee7..e5d8f1e881174 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java @@ -36,13 +36,13 @@ */ public final class SDeclaration extends AStatement { - private final String type; + private final DType type; private final String name; private AExpression expression; private Variable variable = null; - public SDeclaration(Location location, String type, String name, AExpression expression) { + public SDeclaration(Location location, DType type, String name, AExpression expression) { super(location); this.type = Objects.requireNonNull(type); @@ -61,19 +61,15 @@ void extractVariables(Set variables) { @Override void analyze(ScriptRoot scriptRoot, Locals locals) { - Class clazz = scriptRoot.getPainlessLookup().canonicalTypeNameToType(this.type); - - if (clazz == null) { - throw createError(new IllegalArgumentException("Not a type [" + this.type + "].")); - } + DResolvedType resolvedType = type.resolveType(scriptRoot.getPainlessLookup()); if (expression != null) { - expression.expected = clazz; + expression.expected = resolvedType.getType(); expression.analyze(scriptRoot, locals); expression = expression.cast(scriptRoot, locals); } - variable = locals.addVariable(location, clazz, name, false); + variable = locals.addVariable(location, resolvedType.getType(), name, false); } @Override diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/node/NodeToStringTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/node/NodeToStringTests.java index d2832dfdfd5ec..562b6e1e5e90c 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/node/NodeToStringTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/node/NodeToStringTests.java @@ -54,7 +54,7 @@ public class NodeToStringTests extends ESTestCase { public void testEAssignment() { assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration def i))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [def]) i))\n" + " (SExpression (EAssignment (EVariable i) = (ENumeric 2)))\n" + " (SReturn (EVariable i)))", "def i;\n" @@ -63,7 +63,7 @@ public void testEAssignment() { for (String operator : new String[] {"+", "-", "*", "/", "%", "&", "^", "|", "<<", ">>", ">>>"}) { assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration def i (ENumeric 1)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [def]) i (ENumeric 1)))\n" + " (SExpression (EAssignment (EVariable i) " + operator + "= (ENumeric 2)))\n" + " (SReturn (EVariable i)))", "def i = 1;\n" @@ -73,31 +73,31 @@ public void testEAssignment() { // Compound assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration def i))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [def]) i))\n" + " (SReturn (EAssignment (EVariable i) = (ENumeric 2))))", "def i;\n" + "return i = 2"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration def i))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [def]) i))\n" + " (SReturn (EAssignment (EVariable i) ++ post)))", "def i;\n" + "return i++"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration def i))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [def]) i))\n" + " (SReturn (EAssignment (EVariable i) ++ pre)))", "def i;\n" + "return ++i"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration def i))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [def]) i))\n" + " (SReturn (EAssignment (EVariable i) -- post)))", "def i;\n" + "return i--"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration def i))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [def]) i))\n" + " (SReturn (EAssignment (EVariable i) -- pre)))", "def i;\n" + "return --i"); @@ -153,7 +153,8 @@ public void testECallLocal() { public void testECapturingFunctionRef() { assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration Integer x (PCallInvoke (EStatic Integer) valueOf (Args (ENumeric 5)))))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [Integer]) x " + + "(PCallInvoke (EStatic Integer) valueOf (Args (ENumeric 5)))))\n" + " (SReturn (PCallInvoke (PCallInvoke (EStatic Optional) empty) orElseGet (Args (ECapturingFunctionRef x toString)))))", "Integer x = Integer.valueOf(5);\n" + "return Optional.empty().orElseGet(x::toString)"); @@ -349,7 +350,7 @@ public void testEVariable() { assertToString("(SClass (SReturn (EVariable params)))", "return params"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration def a (ENumeric 1)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [def]) a (ENumeric 1)))\n" + " (SReturn (EVariable a)))", "def a = 1;\n" + "return a"); @@ -373,13 +374,13 @@ public void testPField() { assertToString("(SClass (SReturn (PField nullSafe (EVariable params) a)))", "return params?.a"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration int[] a (ENewArray int[] dims (Args (ENumeric 10)))))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int[]]) a (ENewArray int[] dims (Args (ENumeric 10)))))\n" + " (SReturn (PField (EVariable a) length)))", "int[] a = new int[10];\n" + "return a.length"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration org.elasticsearch.painless.FeatureTestObject a" + + " (SDeclBlock (SDeclaration (DUnresolvedType [org.elasticsearch.painless.FeatureTestObject]) a" + " (ENewObj org.elasticsearch.painless.FeatureTestObject)))\n" + " (SExpression (EAssignment (PField (EVariable a) x) = (ENumeric 10)))\n" + " (SReturn (PField (EVariable a) x)))", @@ -511,13 +512,13 @@ public void testPSubShortcut() { public void testSBreak() { assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration int itr (ENumeric 2)))\n" - + " (SDeclBlock (SDeclaration int a (ENumeric 1)))\n" - + " (SDeclBlock (SDeclaration int b (ENumeric 1)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) itr (ENumeric 2)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) a (ENumeric 1)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) b (ENumeric 1)))\n" + " (SDo (EComp (EVariable b) < (ENumeric 1000)) (SBlock\n" + " (SExpression (EAssignment (EVariable itr) ++ post))\n" + " (SIf (EComp (EVariable itr) > (ENumeric 10000)) (SBlock (SBreak)))\n" - + " (SDeclBlock (SDeclaration int tmp (EVariable a)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) tmp (EVariable a)))\n" + " (SExpression (EAssignment (EVariable a) = (EVariable b)))\n" + " (SExpression (EAssignment (EVariable b) = (EBinary (EVariable tmp) + (EVariable b))))))\n" + " (SReturn (EVariable b)))", @@ -539,13 +540,13 @@ public void testSBreak() { public void testSContinue() { assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration int itr (ENumeric 2)))\n" - + " (SDeclBlock (SDeclaration int a (ENumeric 1)))\n" - + " (SDeclBlock (SDeclaration int b (ENumeric 1)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) itr (ENumeric 2)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) a (ENumeric 1)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) b (ENumeric 1)))\n" + " (SDo (EComp (EVariable b) < (ENumeric 1000)) (SBlock\n" + " (SExpression (EAssignment (EVariable itr) ++ post))\n" + " (SIf (EComp (EVariable itr) < (ENumeric 10000)) (SBlock (SContinue)))\n" - + " (SDeclBlock (SDeclaration int tmp (EVariable a)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) tmp (EVariable a)))\n" + " (SExpression (EAssignment (EVariable a) = (EVariable b)))\n" + " (SExpression (EAssignment (EVariable b) = (EBinary (EVariable tmp) + (EVariable b))))))\n" + " (SReturn (EVariable b)))", @@ -567,7 +568,7 @@ public void testSContinue() { public void testSDeclBlock() { assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration def a))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [def]) a))\n" + " (SExpression (EAssignment (EVariable a) = (ENumeric 10)))\n" + " (SReturn (EVariable a)))", "def a;\n" @@ -575,34 +576,34 @@ public void testSDeclBlock() { + "return a"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration def a (ENumeric 10)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [def]) a (ENumeric 10)))\n" + " (SReturn (EVariable a)))", "def a = 10;\n" + "return a"); assertToString( "(SClass\n" + " (SDeclBlock\n" - + " (SDeclaration def a)\n" - + " (SDeclaration def b)\n" - + " (SDeclaration def c))\n" + + " (SDeclaration (DUnresolvedType [def]) a)\n" + + " (SDeclaration (DUnresolvedType [def]) b)\n" + + " (SDeclaration (DUnresolvedType [def]) c))\n" + " (SReturn (EVariable a)))", "def a, b, c;\n" + "return a"); assertToString( "(SClass\n" + " (SDeclBlock\n" - + " (SDeclaration def a (ENumeric 10))\n" - + " (SDeclaration def b (ENumeric 20))\n" - + " (SDeclaration def c (ENumeric 100)))\n" + + " (SDeclaration (DUnresolvedType [def]) a (ENumeric 10))\n" + + " (SDeclaration (DUnresolvedType [def]) b (ENumeric 20))\n" + + " (SDeclaration (DUnresolvedType [def]) c (ENumeric 100)))\n" + " (SReturn (EVariable a)))", "def a = 10, b = 20, c = 100;\n" + "return a"); assertToString( "(SClass\n" + " (SDeclBlock\n" - + " (SDeclaration def a (ENumeric 10))\n" - + " (SDeclaration def b)\n" - + " (SDeclaration def c (ENumeric 100)))\n" + + " (SDeclaration (DUnresolvedType [def]) a (ENumeric 10))\n" + + " (SDeclaration (DUnresolvedType [def]) b)\n" + + " (SDeclaration (DUnresolvedType [def]) c (ENumeric 100)))\n" + " (SReturn (EVariable a)))", "def a = 10, b, c = 100;\n" + "return a"); @@ -610,9 +611,9 @@ public void testSDeclBlock() { "(SClass\n" + " (SIf (PField (EVariable params) a) (SBlock\n" + " (SDeclBlock\n" - + " (SDeclaration def a (ENumeric 10))\n" - + " (SDeclaration def b)\n" - + " (SDeclaration def c (ENumeric 100)))\n" + + " (SDeclaration (DUnresolvedType [def]) a (ENumeric 10))\n" + + " (SDeclaration (DUnresolvedType [def]) b)\n" + + " (SDeclaration (DUnresolvedType [def]) c (ENumeric 100)))\n" + " (SReturn (EVariable a))))\n" + " (SReturn (EBoolean false)))", "if (params.a) {" @@ -625,12 +626,12 @@ public void testSDeclBlock() { public void testSDo() { assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration int itr (ENumeric 2)))\n" - + " (SDeclBlock (SDeclaration int a (ENumeric 1)))\n" - + " (SDeclBlock (SDeclaration int b (ENumeric 1)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) itr (ENumeric 2)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) a (ENumeric 1)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) b (ENumeric 1)))\n" + " (SDo (EComp (EVariable b) < (ENumeric 1000)) (SBlock\n" + " (SExpression (EAssignment (EVariable itr) ++ post))\n" - + " (SDeclBlock (SDeclaration int tmp (EVariable a)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) tmp (EVariable a)))\n" + " (SExpression (EAssignment (EVariable a) = (EVariable b)))\n" + " (SExpression (EAssignment (EVariable b) = (EBinary (EVariable tmp) + (EVariable b))))))\n" + " (SReturn (EVariable b)))", @@ -649,7 +650,7 @@ public void testSDo() { public void testSEach() { assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration int l (ENumeric 0)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) l (ENumeric 0)))\n" + " (SEach String s (EListInit (EString 'cat') (EString 'dog') (EString 'chicken')) (SBlock " + "(SExpression (EAssignment (EVariable l) += (PCallInvoke (EVariable s) length)))))\n" + " (SReturn (EVariable l)))", @@ -660,9 +661,9 @@ public void testSEach() { + "return l"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration int l (ENumeric 0)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) l (ENumeric 0)))\n" + " (SEach String s (EListInit (EString 'cat') (EString 'dog') (EString 'chicken')) (SBlock\n" - + " (SDeclBlock (SDeclaration String s2 (EBinary (EString 'dire ') + (EVariable s))))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [String]) s2 (EBinary (EString 'dire ') + (EVariable s))))\n" + " (SExpression (EAssignment (EVariable l) += (PCallInvoke (EVariable s2) length)))))\n" + " (SReturn (EVariable l)))", "int l = 0;\n" @@ -676,9 +677,9 @@ public void testSEach() { public void testSFor() { assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration int sum (ENumeric 0)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) sum (ENumeric 0)))\n" + " (SFor\n" - + " (SDeclBlock (SDeclaration int i (ENumeric 0)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) i (ENumeric 0)))\n" + " (EComp (EVariable i) < (ENumeric 1000))\n" + " (EAssignment (EVariable i) ++ post)\n" + " (SBlock (SExpression (EAssignment (EVariable sum) += (EVariable i)))))\n" @@ -690,13 +691,13 @@ public void testSFor() { + "return sum"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration int sum (ENumeric 0)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) sum (ENumeric 0)))\n" + " (SFor\n" - + " (SDeclBlock (SDeclaration int i (ENumeric 0)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) i (ENumeric 0)))\n" + " (EComp (EVariable i) < (ENumeric 1000))\n" + " (EAssignment (EVariable i) ++ post)\n" + " (SBlock (SFor\n" - + " (SDeclBlock (SDeclaration int j (ENumeric 0)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) j (ENumeric 0)))\n" + " (EComp (EVariable j) < (ENumeric 1000))\n" + " (EAssignment (EVariable j) ++ post)\n" + " (SBlock (SExpression (EAssignment (EVariable sum) += (EBinary (EVariable i) * (EVariable j))))))))\n" @@ -740,7 +741,7 @@ public void testSIfElse() { + "}"); assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration int i (ENumeric 0)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) i (ENumeric 0)))\n" + " (SIfElse (PField (EVariable param) a)\n" + " (SBlock (SIfElse (PField (EVariable param) b)\n" + " (SBlock (SReturn (EBoolean true)))\n" @@ -789,7 +790,7 @@ public void testSThrow() { public void testSWhile() { assertToString( "(SClass\n" - + " (SDeclBlock (SDeclaration int i (ENumeric 0)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) i (ENumeric 0)))\n" + " (SWhile (EComp (EVariable i) < (ENumeric 10)) (SBlock (SExpression (EAssignment (EVariable i) ++ post))))\n" + " (SReturn (EVariable i)))", "int i = 0;\n" @@ -822,7 +823,7 @@ public void testSFunction() { "(SClass\n" + " (SFunction def a (Args (Pair int i) (Pair int j))\n" + " (SIf (EComp (EVariable i) < (EVariable j)) (SBlock (SReturn (EBoolean true))))\n" - + " (SDeclBlock (SDeclaration int k (EBinary (EVariable i) + (EVariable j))))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) k (EBinary (EVariable i) + (EVariable j))))\n" + " (SReturn (EVariable k)))\n" + " (SReturn (EBoolean true)))", "def a(int i, int j) {\n" @@ -860,7 +861,7 @@ public void testSTryAndSCatch() { + "}"); assertToString( "(SClass (STry (SBlock\n" - + " (SDeclBlock (SDeclaration int i (ENumeric 1)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) i (ENumeric 1)))\n" + " (SReturn (ENumeric 1)))\n" + " (SCatch Exception e (SBlock (SReturn (ENumeric 2))))))", "try {\n" @@ -872,7 +873,7 @@ public void testSTryAndSCatch() { assertToString( "(SClass (STry (SBlock (SReturn (ENumeric 1)))\n" + " (SCatch Exception e (SBlock\n" - + " (SDeclBlock (SDeclaration int i (ENumeric 1)))\n" + + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) i (ENumeric 1)))\n" + " (SReturn (ENumeric 2))))))", "try {\n" + " return 1\n" From 6061b81fc44b27b14f9a1be9fe1843bbe4162096 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Thu, 5 Dec 2019 17:26:35 -0800 Subject: [PATCH 088/686] Dump wildfly log on start failure (#49892) When testing wildfly with Elasticsearch, we currently dump the wildfly log if the test fails. However, when starting wildfly we may fail to find the port number wildfly started on, and fail with no output. This change dumps the wildflog log when failing to find the http or management ports. relates #49374 --- qa/wildfly/build.gradle | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/qa/wildfly/build.gradle b/qa/wildfly/build.gradle index 9fe5656c326d3..4c62d69f180a8 100644 --- a/qa/wildfly/build.gradle +++ b/qa/wildfly/build.gradle @@ -163,8 +163,10 @@ task startWildfly { } } - assert httpPort > 0 - assert managementPort > 0 + if (httpPort == 0 || managementPort == 0) { + String portType = httpPort == 0 ? "http" : "management" + throw new GradleException("Failed to find ${portType} port in wildfly log") + } } } } @@ -188,6 +190,10 @@ if (!Os.isFamily(Os.FAMILY_WINDOWS)) { final TaskExecutionAdapter logDumpListener = new TaskExecutionAdapter() { @Override void afterExecute(final Task task, final TaskState state) { + if (task != startWildfly && task != integTestRunner) { + // we might have been called from a parallel, unrelated task + return + } if (state.failure != null) { final File logFile = new File(wildflyInstall, "standalone/log/server.log") println("\nWildfly server log (from ${logFile}):") @@ -204,12 +210,18 @@ if (!Os.isFamily(Os.FAMILY_WINDOWS)) { } } } + startWildfly.doFirst { + project.gradle.addListener(logDumpListener) + } integTestRunner.doFirst { project.gradle.addListener(logDumpListener) } integTestRunner.doLast { project.gradle.removeListener(logDumpListener) } + startWildfly.doLast { + project.gradle.removeListener(logDumpListener) + } integTestRunner.finalizedBy(stopWildfly) } else { integTest.enabled = false From ac1c8df3094d8f4b248d1ab3c97f0f7e337e1ccc Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Fri, 6 Dec 2019 08:18:13 +0100 Subject: [PATCH 089/686] Reindex sort deprecation warning take 2 (#49855) Moved the deprecation warning to ReindexValidator to ensure it runs early and works with resilient reindex. Also check that the warning is reported back for wait_for_completion=false. Follow-up to #49458 --- .../index/reindex/ReindexValidator.java | 12 +++++++ .../index/reindex/Reindexer.java | 9 ----- .../index/reindex/ReindexSingleNodeTests.java | 2 +- .../rest-api-spec/test/reindex/30_search.yml | 35 +++++++++++++++++++ 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/ReindexValidator.java b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/ReindexValidator.java index 717ba2f0b263a..fffd79e861ade 100644 --- a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/ReindexValidator.java +++ b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/ReindexValidator.java @@ -19,6 +19,8 @@ package org.elasticsearch.index.reindex; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.lucene.util.automaton.Automata; import org.apache.lucene.util.automaton.Automaton; import org.apache.lucene.util.automaton.CharacterRunAutomaton; @@ -32,12 +34,18 @@ import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.search.builder.SearchSourceBuilder; import java.util.List; class ReindexValidator { + private static final Logger logger = LogManager.getLogger(ReindexValidator.class); + private static final DeprecationLogger deprecationLogger = new DeprecationLogger(logger); + static final String SORT_DEPRECATED_MESSAGE = "The sort option in reindex is deprecated. " + + "Instead consider using query filtering to find the desired subset of data."; private final CharacterRunAutomaton remoteWhitelist; private final ClusterService clusterService; @@ -57,6 +65,10 @@ void initialValidation(ReindexRequest request) { ClusterState state = clusterService.state(); validateAgainstAliases(request.getSearchRequest(), request.getDestination(), request.getRemoteInfo(), resolver, autoCreateIndex, state); + SearchSourceBuilder searchSource = request.getSearchRequest().source(); + if (searchSource != null && searchSource.sorts() != null && searchSource.sorts().isEmpty() == false) { + deprecationLogger.deprecated(SORT_DEPRECATED_MESSAGE); + } } static void checkRemoteWhitelist(CharacterRunAutomaton whitelist, RemoteInfo remoteInfo) { diff --git a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/Reindexer.java b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/Reindexer.java index cbe0e8be6572c..39879845bdca1 100644 --- a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/Reindexer.java +++ b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/Reindexer.java @@ -40,7 +40,6 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -52,7 +51,6 @@ import org.elasticsearch.index.reindex.remote.RemoteScrollableHitSource; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptService; -import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; @@ -73,9 +71,6 @@ public class Reindexer { private static final Logger logger = LogManager.getLogger(Reindexer.class); - private static final DeprecationLogger deprecationLogger = new DeprecationLogger(logger); - static final String SORT_DEPRECATED_MESSAGE = "The sort option in reindex is deprecated. " + - "Instead consider using query filtering to find the desired subset of data."; private final ClusterService clusterService; private final Client client; @@ -93,10 +88,6 @@ public class Reindexer { } public void initTask(BulkByScrollTask task, ReindexRequest request, ActionListener listener) { - SearchSourceBuilder searchSource = request.getSearchRequest().source(); - if (searchSource != null && searchSource.sorts() != null && searchSource.sorts().isEmpty() == false) { - deprecationLogger.deprecated(SORT_DEPRECATED_MESSAGE); - } BulkByScrollParallelizationHelper.initTaskState(task, request, client, listener); } diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexSingleNodeTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexSingleNodeTests.java index 14b68ebdf6c6d..118e13fd4f5bf 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexSingleNodeTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexSingleNodeTests.java @@ -55,6 +55,6 @@ public void testDeprecatedSort() { assertHitCount(client().prepareSearch("dest").setSize(0).get(), subsetSize); assertHitCount(client().prepareSearch("dest").setQuery(new RangeQueryBuilder("foo").gte(0).lt(max-subsetSize)).get(), 0); - assertWarnings(Reindexer.SORT_DEPRECATED_MESSAGE); + assertWarnings(ReindexValidator.SORT_DEPRECATED_MESSAGE); } } diff --git a/modules/reindex/src/test/resources/rest-api-spec/test/reindex/30_search.yml b/modules/reindex/src/test/resources/rest-api-spec/test/reindex/30_search.yml index be2d943431030..d021848216517 100644 --- a/modules/reindex/src/test/resources/rest-api-spec/test/reindex/30_search.yml +++ b/modules/reindex/src/test/resources/rest-api-spec/test/reindex/30_search.yml @@ -121,6 +121,41 @@ q: order:1 - match: { hits.total: 1 } +--- +"Sorting deprecated wait_for_completion false": + - skip: + version: " - 7.5.99" + reason: "sort deprecated in 7.6" + features: "warnings" + + - do: + index: + index: test + id: 1 + body: { "order": 1 } + - do: + indices.refresh: {} + + - do: + warnings: + - The sort option in reindex is deprecated. Instead consider using query + filtering to find the desired subset of data. + reindex: + refresh: true + wait_for_completion: false + body: + source: + index: test + sort: order + dest: + index: target + - set: {task: task} + + - do: + tasks.get: + wait_for_completion: true + task_id: $task + --- "max_docs in URL": - skip: From f5973005dced30ca86bc70a78cee2318f428a6ba Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Fri, 6 Dec 2019 08:19:21 +0100 Subject: [PATCH 090/686] [Transform][DOCS]rewrite client ip example to use continuous transform (#49822) adapt the transform example for suspicious client ips to use continuous transform --- docs/reference/transform/examples.asciidoc | 143 ++++++++++++--------- 1 file changed, 83 insertions(+), 60 deletions(-) diff --git a/docs/reference/transform/examples.asciidoc b/docs/reference/transform/examples.asciidoc index 19dd9bbf505d8..f88bff4ce9634 100644 --- a/docs/reference/transform/examples.asciidoc +++ b/docs/reference/transform/examples.asciidoc @@ -54,18 +54,18 @@ POST _transform/_preview ---------------------------------- // TEST[skip:setup kibana sample data] -<1> This is the destination index for the {dataframe}. It is ignored by +<1> This is the destination index for the {transform}. It is ignored by `_preview`. -<2> Two `group_by` fields have been selected. This means the {dataframe} will -contain a unique row per `user` and `customer_id` combination. Within this -dataset both these fields are unique. By including both in the {dataframe} it +<2> Two `group_by` fields have been selected. This means the {transform} will +contain a unique row per `user` and `customer_id` combination. Within this +dataset both these fields are unique. By including both in the {transform} it gives more context to the final results. NOTE: In the example above, condensed JSON formatting has been used for easier readability of the pivot object. -The preview {transforms} API enables you to see the layout of the -{dataframe} in advance, populated with some sample values. For example: +The preview {transforms} API enables you to see the layout of the +{transform} in advance, populated with some sample values. For example: [source,js] ---------------------------------- @@ -86,7 +86,7 @@ The preview {transforms} API enables you to see the layout of the ---------------------------------- // NOTCONSOLE -This {dataframe} makes it easier to answer questions such as: +This {transform} makes it easier to answer questions such as: * Which customers spend the most? @@ -154,7 +154,7 @@ POST _transform/_preview // TEST[skip:setup kibana sample data] <1> Filter the source data to select only flights that were not cancelled. -<2> This is the destination index for the {dataframe}. It is ignored by +<2> This is the destination index for the {transform}. It is ignored by `_preview`. <3> The data is grouped by the `Carrier` field which contains the airline name. <4> This `bucket_script` performs calculations on the results that are returned @@ -181,7 +181,7 @@ carrier: ---------------------------------- // NOTCONSOLE -This {dataframe} makes it easier to answer questions such as: +This {transform} makes it easier to answer questions such as: * Which air carrier has the most delays as a percentage of flight time? @@ -207,21 +207,20 @@ entity is `clientip`. [source,console] ---------------------------------- -POST _transform/_preview +PUT _transform/suspicious_client_ips { "source": { - "index": "kibana_sample_data_logs", - "query": { <1> - "range" : { - "timestamp" : { - "gte" : "now-30d/d" - } - } - } + "index": "kibana_sample_data_logs" }, - "dest" : { <2> + "dest" : { <1> "index" : "sample_weblogs_by_clientip" - }, + }, + "sync" : { <2> + "time": { + "field": "timestamp", + "delay": "60s" + } + }, "pivot": { "group_by": { <3> "clientip": { "terms": { "field": "clientip" } } @@ -275,58 +274,82 @@ POST _transform/_preview ---------------------------------- // TEST[skip:setup kibana sample data] -<1> This range query limits the {transform} to documents that are within the -last 30 days at the point in time the {transform} checkpoint is processed. For -batch {transforms} this occurs once. -<2> This is the destination index for the {dataframe}. It is ignored by -`_preview`. -<3> The data is grouped by the `clientip` field. -<4> This `scripted_metric` performs a distributed operation on the web log data +<1> This is the destination index for the {transform}. +<2> Configures the {transform} to run continuously. It uses the `timestamp` field +to synchronize the source and destination indices. The worst case +ingestion delay is 60 seconds. +<3> The data is grouped by the `clientip` field. +<4> This `scripted_metric` performs a distributed operation on the web log data to count specific types of HTTP responses (error, success, and other). -<5> This `bucket_script` calculates the duration of the `clientip` access based +<5> This `bucket_script` calculates the duration of the `clientip` access based on the results of the aggregation. -The preview shows you that the new index would contain data like this for each -client IP: +After you create the {transform}, you must start it: + +[source,console] +---------------------------------- +POST _transform/suspicious_client_ips/_start +---------------------------------- +// TEST[skip:setup kibana sample data] + +Shortly thereafter, the first results should be available in the destination +index: + +[source,console] +---------------------------------- +GET sample_weblogs_by_clientip/_search +---------------------------------- +// TEST[skip:setup kibana sample data] + +The search result shows you data like this for each client IP: [source,js] ---------------------------------- -{ - "preview" : [ - { - "geo" : { - "src_dc" : 12.0, - "dest_dc" : 9.0 - }, - "clientip" : "0.72.176.46", - "agent_dc" : 3.0, - "responses" : { - "total" : 14.0, - "counts" : { - "other" : 0, - "success" : 14, - "error" : 0 + "hits" : [ + { + "_index" : "sample_weblogs_by_clientip", + "_id" : "MOeHH_cUL5urmartKj-b5UQAAAAAAAAA", + "_score" : 1.0, + "_source" : { + "geo" : { + "src_dc" : 2.0, + "dest_dc" : 2.0 + }, + "clientip" : "0.72.176.46", + "agent_dc" : 2.0, + "bytes_sum" : 4422.0, + "responses" : { + "total" : 2.0, + "counts" : { + "other" : 0, + "success" : 2, + "error" : 0 + } + }, + "url_dc" : 2.0, + "timestamp" : { + "duration_ms" : 5.2191698E8, + "min" : "2019-11-25T07:51:57.333Z", + "max" : "2019-12-01T08:50:34.313Z" + } } - }, - "bytes_sum" : 74808.0, - "timestamp" : { - "duration_ms" : 4.919943239E9, - "min" : "2019-06-17T07:51:57.333Z", - "max" : "2019-08-13T06:31:00.572Z" - }, - "url_dc" : 11.0 - }, - ... - } ----------------------------------- + } + ] +---------------------------------- // NOTCONSOLE -This {dataframe} makes it easier to answer questions such as: +NOTE: Like other Kibana sample data sets, the web log sample dataset contains +timestamps relative to when you installed it, including timestamps in the future. +The {ctransform} will pick up the data points once they are in the past. If you +installed the web log sample dataset some time ago, you can uninstall and +reinstall it and the timestamps will change. + +This {transform} makes it easier to answer questions such as: * Which client IPs are transferring the most amounts of data? * Which client IPs are interacting with a high number of different URLs? - + * Which client IPs have high error rates? - + * Which client IPs are interacting with a high number of destination countries? From e89bc3a1a968acb26d8e640cb44c84b50e559251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Fri, 6 Dec 2019 10:23:01 +0100 Subject: [PATCH 091/686] [DOCS] Fixes attribute in transforms overview. (#49898) --- docs/reference/transform/overview.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/transform/overview.asciidoc b/docs/reference/transform/overview.asciidoc index 50930b00c5cb3..07c17f778abaf 100644 --- a/docs/reference/transform/overview.asciidoc +++ b/docs/reference/transform/overview.asciidoc @@ -39,7 +39,7 @@ The {transform} performs a composite aggregation that paginates through all the data defined by the source index query. The output of the aggregation is stored in a destination index. Each time the {transform} queries the source index, it creates a _checkpoint_. You can decide whether you want the {transform} to run -once (batch {transform}) or continuously ({transform}). A batch {transform} is a +once (batch {transform}) or continuously ({ctransform}). A batch {transform} is a single operation that has a single checkpoint. {ctransforms-cap} continually increment and process checkpoints as new source data is ingested. From ffdb27b42d9d5609306b05266350d16e94ab96ef Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 6 Dec 2019 10:28:02 +0100 Subject: [PATCH 092/686] Cleanup Old index-N Blobs in Repository Cleanup (#49862) * Cleanup Old index-N Blobs in Repository Cleanup Repository cleanup didn't deal with old index-N, this change adds cleaning up all old index-N found in the repository. * add test --- .../blobstore/BlobStoreRepository.java | 3 ++ .../BlobStoreRepositoryCleanupIT.java | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index ba670c2d6f01d..838bede23bf54 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -731,6 +731,9 @@ private List staleRootBlobs(RepositoryData repositoryData, Set r return false; } return allSnapshotIds.contains(foundUUID) == false; + } else if (blob.startsWith(INDEX_FILE_PREFIX)) { + // TODO: Include the current generation here once we remove keeping index-(N-1) around from #writeIndexGen + return repositoryData.getGenId() > Long.parseLong(blob.substring(INDEX_FILE_PREFIX.length())); } return false; } diff --git a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryCleanupIT.java b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryCleanupIT.java index 7ee49e53e2658..fd195123fc0a9 100644 --- a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryCleanupIT.java +++ b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryCleanupIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.repositories.blobstore; import org.elasticsearch.action.ActionRunnable; +import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.RepositoryCleanupInProgress; import org.elasticsearch.common.settings.Settings; @@ -26,13 +27,16 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.snapshots.AbstractSnapshotIntegTestCase; +import org.elasticsearch.snapshots.SnapshotState; import org.elasticsearch.test.ESIntegTestCase; import java.io.ByteArrayInputStream; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertThrows; +import static org.hamcrest.Matchers.is; @ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0) public class BlobStoreRepositoryCleanupIT extends AbstractSnapshotIntegTestCase { @@ -107,4 +111,40 @@ private String startBlockedCleanup(String repoName) throws Exception { waitForBlock(masterNode, repoName, TimeValue.timeValueSeconds(60)); return masterNode; } + + public void testCleanupOldIndexN() throws ExecutionException, InterruptedException { + internalCluster().startNodes(Settings.EMPTY); + + final String repoName = "test-repo"; + logger.info("--> creating repository"); + assertAcked(client().admin().cluster().preparePutRepository(repoName).setType("fs").setSettings(Settings.builder() + .put("location", randomRepoPath()) + .put("compress", randomBoolean()) + .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); + + logger.info("--> create three snapshots"); + for (int i = 0; i < 3; ++i) { + CreateSnapshotResponse createSnapshotResponse = client().admin().cluster().prepareCreateSnapshot(repoName, "test-snap-" + i) + .setWaitForCompletion(true).get(); + assertThat(createSnapshotResponse.getSnapshotInfo().state(), is(SnapshotState.SUCCESS)); + } + + final RepositoriesService service = internalCluster().getInstance(RepositoriesService.class, internalCluster().getMasterName()); + final BlobStoreRepository repository = (BlobStoreRepository) service.repository(repoName); + + logger.info("--> write two outdated index-N blobs"); + for (int i = 0; i < 2; ++i) { + final PlainActionFuture createOldIndexNFuture = PlainActionFuture.newFuture(); + final int generation = i; + repository.threadPool().generic().execute(ActionRunnable.run(createOldIndexNFuture, () -> repository.blobStore() + .blobContainer(repository.basePath()).writeBlob(BlobStoreRepository.INDEX_FILE_PREFIX + generation, + new ByteArrayInputStream(new byte[1]), 1, true))); + createOldIndexNFuture.get(); + } + + logger.info("--> cleanup repository"); + client().admin().cluster().prepareCleanupRepository(repoName).get(); + + BlobStoreTestUtil.assertConsistency(repository, repository.threadPool().generic()); + } } From b7ffb2b52a478fbadaa792cca3b5967c940134da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Fri, 6 Dec 2019 13:24:22 +0100 Subject: [PATCH 093/686] [DOCS] Fixes classification evaluation example response. (#49905) --- .../ml/df-analytics/apis/evaluate-dfanalytics.asciidoc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/reference/ml/df-analytics/apis/evaluate-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/evaluate-dfanalytics.asciidoc index 0b855ef731fad..4576f465a7604 100644 --- a/docs/reference/ml/df-analytics/apis/evaluate-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/evaluate-dfanalytics.asciidoc @@ -78,6 +78,8 @@ Available evaluation types: [[ml-evaluate-dfanalytics-example]] ==== {api-examples-title} + +[[ml-evaluate-binary-soft-class-example]] ===== Binary soft classification [source,console] @@ -139,6 +141,7 @@ The API returns the following results: ---- +[[ml-evaluate-regression-example]] ===== {regression-cap} [source,console] @@ -252,6 +255,7 @@ performance. This is required in order to evaluate results. calculated by the {reganalysis}. +[[ml-evaluate-classification-example]] ===== {classification-cap} @@ -311,14 +315,14 @@ The API returns the following result: "predicted_classes" : [ { "predicted_class" : "dog", - "count" : 11 + "count" : 7 }, { "predicted_class" : "cat", "count" : 4 } ], - "other_predicted_class_doc_count" : 4 + "other_predicted_class_doc_count" : 0 } ], "other_actual_class_count" : 0 From a16ebafc0a91c6c21379d8fa058a380f4edddf00 Mon Sep 17 00:00:00 2001 From: cachedout Date: Fri, 6 Dec 2019 12:28:20 +0000 Subject: [PATCH 094/686] APM system_user (#47668) * Add test for APM beats index perms * Grant monitoring index privs to apm_system user * Review feedback * Fix compilation problem --- .../security/authz/store/ReservedRolesStore.java | 7 ++++++- .../authz/store/ReservedRolesStoreTests.java | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index ed80944a3764e..7e06bbf64d999 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -148,7 +148,12 @@ private static Map initializeReservedRoles() { }, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) .put(UsernamesField.APM_ROLE, new RoleDescriptor(UsernamesField.APM_ROLE, - new String[] { "monitor", MonitoringBulkAction.NAME}, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) + new String[] { "monitor", MonitoringBulkAction.NAME}, + new RoleDescriptor.IndicesPrivileges[]{ + RoleDescriptor.IndicesPrivileges.builder() + .indices(".monitoring-beats-*").privileges("create_index", "create_doc").build() + }, + null, MetadataUtils.DEFAULT_RESERVED_METADATA)) .put("apm_user", new RoleDescriptor("apm_user", null, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder().indices("apm-*") diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index 97fd4172b4ce6..533962efd5a31 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -892,7 +892,7 @@ public void testBeatsSystemRole() { final String index = ".monitoring-beats-" + randomIntBetween(0, 5);; - logger.info("index name [{}]", index); + logger.info("beats monitoring index name [{}]", index); assertThat(beatsSystemRole.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false)); assertThat(beatsSystemRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(".reporting"), is(false)); assertThat(beatsSystemRole.indices().allowedIndicesMatcher("indices:foo").test(randomAlphaOfLengthBetween(8, 24)), @@ -930,7 +930,20 @@ public void testAPMSystemRole() { assertThat(APMSystemRole.indices().allowedIndicesMatcher("indices:foo").test(randomAlphaOfLengthBetween(8, 24)), is(false)); + final String index = ".monitoring-beats-" + randomIntBetween(10, 15); + logger.info("APM beats monitoring index name [{}]", index); + + assertThat(APMSystemRole.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(index), is(true)); + assertThat(APMSystemRole.indices().allowedIndicesMatcher("indices:data/write/index:op_type/create").test(index), is(true)); + assertThat(APMSystemRole.indices().allowedIndicesMatcher(DeleteAction.NAME).test(index), is(false)); + assertThat(APMSystemRole.indices().allowedIndicesMatcher(BulkAction.NAME).test(index), is(true)); + + assertThat(APMSystemRole.indices().allowedIndicesMatcher("indices:data/write/index:op_type/index").test(index), is(false)); + assertThat(APMSystemRole.indices().allowedIndicesMatcher( + "indices:data/write/index:op_type/" + randomAlphaOfLengthBetween(3,5)).test(index), is(false)); + assertNoAccessAllowed(APMSystemRole, RestrictedIndicesNames.RESTRICTED_NAMES); + } public void testAPMUserRole() { From f149a4ce8860a66f9aca412d5adbc0533381af74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Fri, 6 Dec 2019 13:31:10 +0100 Subject: [PATCH 095/686] Log whole analytics stats when the state assertion fails (#49906) --- .../ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java index b3b58a2d4fcbb..0b9e2c19961d8 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java @@ -174,7 +174,7 @@ protected void assertIsStopped(String id) { GetDataFrameAnalyticsStatsAction.Response.Stats stats = getAnalyticsStats(id); assertThat(stats.getId(), equalTo(id)); assertThat(stats.getFailureReason(), is(nullValue())); - assertThat(stats.getState(), equalTo(DataFrameAnalyticsState.STOPPED)); + assertThat("Stats were: " + Strings.toString(stats), stats.getState(), equalTo(DataFrameAnalyticsState.STOPPED)); } protected void assertProgress(String id, int reindexing, int loadingData, int analyzing, int writingResults) { From f63dc8e8081910a1bdf44363c306aa7e6e72f0f9 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Fri, 6 Dec 2019 14:33:02 +0100 Subject: [PATCH 096/686] Add tests for ingesting CBOR data attachments (#49715) Our docs specifically mention that CBOR is supported when ingesting attachments. However this is not tested anywhere. This adds a test, that uses specifically CBOR format in its IndexRequest and another one that behaves like CBOR in the ingest attachment unit tests. --- .../attachment/AttachmentProcessorTests.java | 11 +++++-- .../ingest/IngestServiceTests.java | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/plugins/ingest-attachment/src/test/java/org/elasticsearch/ingest/attachment/AttachmentProcessorTests.java b/plugins/ingest-attachment/src/test/java/org/elasticsearch/ingest/attachment/AttachmentProcessorTests.java index 5658eb567177b..11f96a51b10ae 100644 --- a/plugins/ingest-attachment/src/test/java/org/elasticsearch/ingest/attachment/AttachmentProcessorTests.java +++ b/plugins/ingest-attachment/src/test/java/org/elasticsearch/ingest/attachment/AttachmentProcessorTests.java @@ -284,7 +284,7 @@ private Map parseDocument(String file, AttachmentProcessor proce private Map parseDocument(String file, AttachmentProcessor processor, Map optionalFields) throws Exception { Map document = new HashMap<>(); - document.put("source_field", getAsBase64(file)); + document.put("source_field", getAsBinaryOrBase64(file)); document.putAll(optionalFields); IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); @@ -335,11 +335,16 @@ public void testIndexedChars() throws Exception { assertThat(attachmentData.get("content_length"), is(56L)); } - private String getAsBase64(String filename) throws Exception { + private Object getAsBinaryOrBase64(String filename) throws Exception { String path = "/org/elasticsearch/ingest/attachment/test/sample-files/" + filename; try (InputStream is = AttachmentProcessorTests.class.getResourceAsStream(path)) { byte bytes[] = IOUtils.toByteArray(is); - return Base64.getEncoder().encodeToString(bytes); + // behave like CBOR from time to time + if (rarely()) { + return bytes; + } else { + return Base64.getEncoder().encodeToString(bytes); + } } } } diff --git a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java index 5400956d076c3..7890d30f6c4e3 100644 --- a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java @@ -46,7 +46,9 @@ import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.cbor.CborXContent; import org.elasticsearch.index.VersionType; import org.elasticsearch.plugins.IngestPlugin; import org.elasticsearch.script.MockScriptEngine; @@ -61,6 +63,7 @@ import org.mockito.ArgumentMatcher; import org.mockito.invocation.InvocationOnMock; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -70,6 +73,7 @@ import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -1202,6 +1206,35 @@ public Map getProcessors(Processor.Parameters paramet assertThat(counter.get(), equalTo(2)); } + public void testCBORParsing() throws Exception { + AtomicReference reference = new AtomicReference<>(); + Consumer executor = doc -> reference.set(doc.getFieldValueAsBytes("data")); + final IngestService ingestService = createWithProcessors(Collections.singletonMap("foo", + (factories, tag, config) -> new FakeProcessor("foo", tag, executor))); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); + ClusterState previousClusterState = clusterState; + PutPipelineRequest putRequest = new PutPipelineRequest("_id", + new BytesArray("{\"processors\": [{\"foo\" : {}}]}"), XContentType.JSON); + clusterState = IngestService.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + assertThat(ingestService.getPipeline("_id"), notNullValue()); + + try (XContentBuilder builder = CborXContent.contentBuilder()) { + builder.startObject(); + builder.field("data", "This is my data".getBytes(StandardCharsets.UTF_8)); + builder.endObject(); + + IndexRequest indexRequest = + new IndexRequest("_index").id("_doc-id").source(builder).setPipeline("_id").setFinalPipeline("_none"); + + ingestService.executeBulkRequest(1, Collections.singletonList(indexRequest), + (integer, e) -> {}, (thread, e) -> {}, indexReq -> {}); + } + + assertThat(reference.get(), is(instanceOf(byte[].class))); + } + private IngestDocument eqIndexTypeId(final Map source) { return argThat(new IngestDocumentMatcher("_index", "_type", "_id", -3L, VersionType.INTERNAL, source)); } From ab9baeffc40e4098898abbb4e8212b32eb0ddb40 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 6 Dec 2019 15:57:00 +0200 Subject: [PATCH 097/686] Always return 401 for not valid tokens (#49736) Return a 401 in all cases when a request is submitted with an access token that we can't consume. Before this change, we would throw a 500 when a request came in with an access token that we had generated but was then invalidated/expired and deleted from the tokens index. Resolves: #38866 --- .../xpack/security/authc/TokenService.java | 8 ++-- .../security/authc/TokenAuthIntegTests.java | 43 ++++++++++++++++++- .../security/authc/TokenServiceTests.java | 40 ++++++++++++++++- 3 files changed, 86 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 689a6db034130..42d582997d827 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -421,7 +421,7 @@ private void getUserTokenFromId(String userTokenId, Version tokenVersion, Action } else { final GetRequest getRequest = client.prepareGet(tokensIndex.aliasName(), getTokenDocumentId(userTokenId)).request(); - final Consumer onFailure = ex -> listener.onFailure(traceLog("decode token", userTokenId, ex)); + final Consumer onFailure = ex -> listener.onFailure(traceLog("get token from id", userTokenId, ex)); tokensIndex.checkIndexVersionThenExecute( ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() +"]", userTokenId, ex)), () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, getRequest, @@ -441,8 +441,10 @@ private void getUserTokenFromId(String userTokenId, Version tokenVersion, Action listener.onResponse(UserToken.fromSourceMap(userTokenSource)); } } else { - onFailure.accept( - new IllegalStateException("token document is missing and must be present")); + // The chances of a random token string decoding to something that we can read is minimal, so + // we assume that this was a token we have created but is now expired/revoked and deleted + logger.trace("The access token [{}] is expired and already deleted", userTokenId); + listener.onResponse(null); } }, e -> { // if the index or the shard is not there / available we assume that diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java index 98f09ef631e90..d56365a21a403 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java @@ -7,6 +7,7 @@ import org.apache.directory.api.util.Strings; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.Version; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; import org.elasticsearch.action.search.SearchRequest; @@ -23,6 +24,7 @@ import org.elasticsearch.client.security.InvalidateTokenRequest; import org.elasticsearch.client.security.InvalidateTokenResponse; import org.elasticsearch.cluster.ack.ClusterStateUpdateResponse; +import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.query.QueryBuilders; @@ -171,6 +173,7 @@ public void testExpiredTokensDeletedAfterExpiration() throws Exception { restClient.security().invalidateToken(new InvalidateTokenRequest("fooobar", null, null, null), SECURITY_REQUEST_OPTIONS)); assertThat(e.getMessage(), containsString("token malformed")); + assertThat(e.status(), equalTo(RestStatus.UNAUTHORIZED)); } restClient.indices().refresh(new RefreshRequest(RestrictedIndicesNames.SECURITY_TOKENS_ALIAS), SECURITY_REQUEST_OPTIONS); SearchResponse searchResponse = restClient.search(new SearchRequest(RestrictedIndicesNames.SECURITY_TOKENS_ALIAS) @@ -455,7 +458,36 @@ public void testClientCredentialsGrant() throws Exception { ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> restClient.security().authenticate(tokenAuthOptions)); - assertEquals(RestStatus.UNAUTHORIZED, e.status()); + assertThat(e.status(), equalTo(RestStatus.UNAUTHORIZED)); + } + + public void testAuthenticateWithWrongToken() throws Exception { + final RestHighLevelClient restClient = new TestRestHighLevelClient(); + CreateTokenResponse response = restClient.security().createToken(CreateTokenRequest.passwordGrant( + SecuritySettingsSource.TEST_USER_NAME, SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()), SECURITY_REQUEST_OPTIONS); + assertNotNull(response.getRefreshToken()); + // First check that the correct access token works by getting cluster health with token + assertNoTimeout(client() + .filterWithHeader(Collections.singletonMap("Authorization", "Bearer " + response.getAccessToken())) + .admin().cluster().prepareHealth().get()); + // Now attempt to authenticate with an invalid access token string + RequestOptions wrongAuthOptions = + RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + randomAlphaOfLengthBetween(0, 128)).build(); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, + () -> restClient.security().authenticate(wrongAuthOptions)); + assertThat(e.status(), equalTo(RestStatus.UNAUTHORIZED)); + // Now attempt to authenticate with an invalid access token with valid structure (pre 7.2) + RequestOptions wrongAuthOptionsPre72 = + RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + generateAccessToken(Version.V_7_1_0)).build(); + ElasticsearchStatusException e1 = expectThrows(ElasticsearchStatusException.class, + () -> restClient.security().authenticate(wrongAuthOptionsPre72)); + assertThat(e1.status(), equalTo(RestStatus.UNAUTHORIZED)); + // Now attempt to authenticate with an invalid access token with valid structure (after 7.2) + RequestOptions wrongAuthOptionsAfter72 = + RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + generateAccessToken(Version.V_7_4_0)).build(); + ElasticsearchStatusException e2 = expectThrows(ElasticsearchStatusException.class, + () -> restClient.security().authenticate(wrongAuthOptionsAfter72)); + assertThat(e2.status(), equalTo(RestStatus.UNAUTHORIZED)); } @Before @@ -476,4 +508,13 @@ public void testMetadataIsNotSentToClient() { ClusterStateResponse clusterStateResponse = client().admin().cluster().prepareState().setCustoms(true).get(); assertFalse(clusterStateResponse.getState().customs().containsKey(TokenMetaData.TYPE)); } + + private String generateAccessToken(Version version) throws Exception { + TokenService tokenService = internalCluster().getInstance(TokenService.class); + String accessTokenString = UUIDs.randomBase64UUID(); + if (version.onOrAfter(TokenService.VERSION_ACCESS_TOKENS_AS_UUIDS)) { + accessTokenString = TokenService.hashTokenString(accessTokenString); + } + return tokenService.prependVersionAndEncodeAccessToken(version, accessTokenString); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index e8585d9c6cb97..f17bdc980bcaf 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -598,7 +598,7 @@ public void testMalformedToken() throws Exception { final int numBytes = randomIntBetween(1, TokenService.MINIMUM_BYTES + 32); final byte[] randomBytes = new byte[numBytes]; random().nextBytes(randomBytes); - TokenService tokenService = createTokenService(Settings.EMPTY, systemUTC()); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); storeTokenHeader(requestContext, Base64.getEncoder().encodeToString(randomBytes)); @@ -610,6 +610,36 @@ public void testMalformedToken() throws Exception { } } + public void testNotValidPre72Tokens() throws Exception { + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); + // mock another random token so that we don't find a token in TokenService#getUserTokenFromId + Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); + mockGetTokenFromId(tokenService, UUIDs.randomBase64UUID(), authentication, false); + ThreadContext requestContext = new ThreadContext(Settings.EMPTY); + storeTokenHeader(requestContext, generateAccessToken(tokenService, Version.V_7_1_0)); + + try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { + PlainActionFuture future = new PlainActionFuture<>(); + tokenService.getAndValidateToken(requestContext, future); + assertNull(future.get()); + } + } + + public void testNotValidAfter72Tokens() throws Exception { + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); + // mock another random token so that we don't find a token in TokenService#getUserTokenFromId + Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); + mockGetTokenFromId(tokenService, UUIDs.randomBase64UUID(), authentication, false); + ThreadContext requestContext = new ThreadContext(Settings.EMPTY); + storeTokenHeader(requestContext, generateAccessToken(tokenService, randomFrom(Version.V_7_2_0, Version.V_7_3_2))); + + try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { + PlainActionFuture future = new PlainActionFuture<>(); + tokenService.getAndValidateToken(requestContext, future); + assertNull(future.get()); + } + } + public void testIndexNotAvailable() throws Exception { TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); @@ -821,4 +851,12 @@ private DiscoveryNode addAnotherDataNodeWithVersion(ClusterService clusterServic return anotherDataNode; } + private String generateAccessToken(TokenService tokenService, Version version) throws Exception { + String accessTokenString = UUIDs.randomBase64UUID(); + if (version.onOrAfter(TokenService.VERSION_ACCESS_TOKENS_AS_UUIDS)) { + accessTokenString = TokenService.hashTokenString(accessTokenString); + } + return tokenService.prependVersionAndEncodeAccessToken(version, accessTokenString); + } + } From e80752eeb044593c71907317307206d7282dd205 Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Fri, 6 Dec 2019 18:09:56 +0200 Subject: [PATCH 098/686] Refactor usage of NamedExpression (#49693) To recap, Attributes form the properties of a derived table. Each LogicalPlan has Attributes as output since each one can be part of a query and as such its result are sent to its consumer. This change essentially removes the name id comparison so any changes applied to existing expressions should work as long as the said expressions are semantically equivalent. This change enforces the hashCode and equals which has the side-effect of using hashCode as identifiers for each expression. By removing any property from an Attribute, the various components need to look the original source for comparison which, while annoying, should prevent a reference from getting out of sync with its source due to optimizations. Essentially going forward there are only 3 types of NamedExpressions: Alias - user define (implicit or explicit) name FieldAttribute - field backed by Elasticsearch ReferenceAttribute - a reference to another source acting as an Attribute. Typically the Attribute of an Alias. * Remove the usage of NamedExpression as basis for all Expressions. Instead, restrict their use only for named context, such as projections by using Aliasing instead. * Remove different types of Attributes and allow only FieldAttribute, UnresolvedAttribute and ReferenceAttribute. To avoid issues with rewrites, resolve the references inside the QueryContainer so the information always stays on the source. * Side-effect, simplify the rules as the state for InnerAggs doesn't have to be contained anymore. * Improve ResolveMissingRef rule to handle references to named non-singular expression tree against the same expression used up the tree. --- .../sql/qa/single_node/CliExplainIT.java | 24 +- .../xpack/sql/qa/rest/RestSqlTestCase.java | 60 +- .../sql/qa/src/main/resources/agg.csv-spec | 24 +- .../sql/qa/src/main/resources/math.csv-spec | 4 +- .../xpack/sql/analysis/analyzer/Analyzer.java | 215 ++- .../xpack/sql/analysis/analyzer/Verifier.java | 114 +- .../xpack/sql/execution/search/Querier.java | 9 +- .../xpack/sql/expression/Alias.java | 36 +- .../xpack/sql/expression/Attribute.java | 56 +- .../xpack/sql/expression/AttributeMap.java | 12 +- .../xpack/sql/expression/Exists.java | 2 +- .../xpack/sql/expression/Expression.java | 9 +- .../xpack/sql/expression/Expressions.java | 69 +- .../xpack/sql/expression/FieldAttribute.java | 10 +- .../xpack/sql/expression/Literal.java | 69 +- .../sql/expression/LiteralAttribute.java | 49 - .../{ExpressionId.java => NameId.java} | 14 +- .../xpack/sql/expression/NamedExpression.java | 36 +- .../xpack/sql/expression/Order.java | 2 +- .../sql/expression/ReferenceAttribute.java | 41 + .../xpack/sql/expression/ScalarSubquery.java | 2 +- .../sql/expression/SubQueryExpression.java | 8 +- .../xpack/sql/expression/TypedAttribute.java | 2 +- .../xpack/sql/expression/UnresolvedAlias.java | 13 +- .../sql/expression/UnresolvedAttribute.java | 17 +- .../expression/UnresolvedNamedExpression.java | 10 +- .../xpack/sql/expression/UnresolvedStar.java | 11 +- .../sql/expression/function/Function.java | 71 +- .../function/FunctionAttribute.java | 39 - .../sql/expression/function/Functions.java | 15 - .../xpack/sql/expression/function/Score.java | 8 +- .../expression/function/ScoreAttribute.java | 58 - .../function/UnresolvedFunction.java | 17 +- .../function/aggregate/AggregateFunction.java | 37 +- .../aggregate/AggregateFunctionAttribute.java | 94 -- .../expression/function/aggregate/Count.java | 43 +- .../function/aggregate/InnerAggregate.java | 34 +- .../function/grouping/GroupingFunction.java | 13 +- .../grouping/GroupingFunctionAttribute.java | 56 - .../function/scalar/ScalarFunction.java | 11 - .../scalar/ScalarFunctionAttribute.java | 95 -- .../function/scalar/UnaryScalarFunction.java | 7 +- .../scalar/datetime/BaseDateTimeFunction.java | 10 +- .../function/scalar/geo/StWkttosql.java | 2 +- .../expression/function/scalar/math/E.java | 2 +- .../expression/function/scalar/math/Pi.java | 2 +- .../function/scalar/string/Concat.java | 4 +- .../xpack/sql/expression/gen/script/Agg.java | 38 +- .../sql/expression/gen/script/Grouping.java | 8 +- .../expression/gen/script/ParamsBuilder.java | 8 +- .../expression/gen/script/ScriptWeaver.java | 44 +- .../expression/predicate/BinaryPredicate.java | 5 + .../xpack/sql/expression/predicate/Range.java | 26 - .../predicate/conditional/IfConditional.java | 27 +- .../predicate/operator/comparison/In.java | 2 +- .../xpack/sql/optimizer/Optimizer.java | 1194 +++++++---------- .../xpack/sql/parser/ExpressionBuilder.java | 7 +- .../xpack/sql/plan/logical/Pivot.java | 119 +- .../xpack/sql/plan/logical/SubQueryAlias.java | 9 +- .../xpack/sql/planner/QueryFolder.java | 436 ++++-- .../xpack/sql/planner/QueryTranslator.java | 360 +---- .../xpack/sql/querydsl/agg/Aggs.java | 15 +- .../sql/querydsl/container/AggregateSort.java | 46 + .../querydsl/container/QueryContainer.java | 174 +-- .../elasticsearch/xpack/sql/tree/Node.java | 30 +- .../analyzer/VerifierErrorMessagesTests.java | 6 +- .../search/SourceGeneratorTests.java | 13 +- .../sql/expression/AttributeMapTests.java | 4 +- .../sql/expression/ExpressionIdTests.java | 4 +- .../xpack/sql/expression/LiteralTests.java | 2 +- .../expression/UnresolvedAttributeTests.java | 4 +- .../scalar/DatabaseFunctionTests.java | 8 +- .../function/scalar/UserFunctionTests.java | 14 +- .../predicate/conditional/CaseTests.java | 6 +- .../predicate/conditional/IifTests.java | 4 - .../xpack/sql/optimizer/OptimizerTests.java | 175 ++- .../xpack/sql/parser/ExpressionTests.java | 12 +- .../xpack/sql/parser/SqlParserTests.java | 32 +- .../xpack/sql/planner/QueryFolderTests.java | 94 +- .../sql/planner/QueryTranslatorTests.java | 53 +- .../container/QueryContainerTests.java | 9 +- 81 files changed, 1946 insertions(+), 2578 deletions(-) delete mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/LiteralAttribute.java rename x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/{ExpressionId.java => NameId.java} (77%) create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ReferenceAttribute.java delete mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionAttribute.java delete mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/ScoreAttribute.java delete mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/AggregateFunctionAttribute.java delete mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/grouping/GroupingFunctionAttribute.java delete mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunctionAttribute.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/AggregateSort.java diff --git a/x-pack/plugin/sql/qa/single-node/src/test/java/org/elasticsearch/xpack/sql/qa/single_node/CliExplainIT.java b/x-pack/plugin/sql/qa/single-node/src/test/java/org/elasticsearch/xpack/sql/qa/single_node/CliExplainIT.java index af52e50348c1b..c7069f18e0265 100644 --- a/x-pack/plugin/sql/qa/single-node/src/test/java/org/elasticsearch/xpack/sql/qa/single_node/CliExplainIT.java +++ b/x-pack/plugin/sql/qa/single-node/src/test/java/org/elasticsearch/xpack/sql/qa/single_node/CliExplainIT.java @@ -19,19 +19,19 @@ public void testExplainBasic() throws IOException { assertThat(command("EXPLAIN (PLAN PARSED) SELECT * FROM test"), containsString("plan")); assertThat(readLine(), startsWith("----------")); assertThat(readLine(), startsWith("With[{}]")); - assertThat(readLine(), startsWith("\\_Project[[?*]]")); + assertThat(readLine(), startsWith("\\_Project[[?* AS ?]]")); assertThat(readLine(), startsWith(" \\_UnresolvedRelation[test]")); assertEquals("", readLine()); assertThat(command("EXPLAIN " + (randomBoolean() ? "" : "(PLAN ANALYZED) ") + "SELECT * FROM test"), containsString("plan")); assertThat(readLine(), startsWith("----------")); - assertThat(readLine(), startsWith("Project[[test_field{f}#")); + assertThat(readLine(), startsWith("Project[[test.test_field{f}#")); assertThat(readLine(), startsWith("\\_EsRelation[test][test_field{f}#")); assertEquals("", readLine()); assertThat(command("EXPLAIN (PLAN OPTIMIZED) SELECT * FROM test"), containsString("plan")); assertThat(readLine(), startsWith("----------")); - assertThat(readLine(), startsWith("Project[[test_field{f}#")); + assertThat(readLine(), startsWith("Project[[test.test_field{f}#")); assertThat(readLine(), startsWith("\\_EsRelation[test][test_field{f}#")); assertEquals("", readLine()); @@ -63,23 +63,23 @@ public void testExplainWithWhere() throws IOException { assertThat(command("EXPLAIN (PLAN PARSED) SELECT * FROM test WHERE i = 2"), containsString("plan")); assertThat(readLine(), startsWith("----------")); assertThat(readLine(), startsWith("With[{}]")); - assertThat(readLine(), startsWith("\\_Project[[?*]]")); - assertThat(readLine(), startsWith(" \\_Filter[Equals[?i,2")); + assertThat(readLine(), startsWith("\\_Project[[?* AS ?]]")); + assertThat(readLine(), startsWith(" \\_Filter[?i == 2[INTEGER]]")); assertThat(readLine(), startsWith(" \\_UnresolvedRelation[test]")); assertEquals("", readLine()); assertThat(command("EXPLAIN " + (randomBoolean() ? "" : "(PLAN ANALYZED) ") + "SELECT * FROM test WHERE i = 2"), containsString("plan")); assertThat(readLine(), startsWith("----------")); - assertThat(readLine(), startsWith("Project[[i{f}#")); - assertThat(readLine(), startsWith("\\_Filter[Equals[i")); + assertThat(readLine(), startsWith("Project[[test.i{f}#")); + assertThat(readLine(), startsWith("\\_Filter[test.i{f}#")); assertThat(readLine(), startsWith(" \\_EsRelation[test][i{f}#")); assertEquals("", readLine()); assertThat(command("EXPLAIN (PLAN OPTIMIZED) SELECT * FROM test WHERE i = 2"), containsString("plan")); assertThat(readLine(), startsWith("----------")); - assertThat(readLine(), startsWith("Project[[i{f}#")); - assertThat(readLine(), startsWith("\\_Filter[Equals[i")); + assertThat(readLine(), startsWith("Project[[test.i{f}#")); + assertThat(readLine(), startsWith("\\_Filter[test.i{f}")); assertThat(readLine(), startsWith(" \\_EsRelation[test][i{f}#")); assertEquals("", readLine()); @@ -119,20 +119,20 @@ public void testExplainWithCount() throws IOException { assertThat(command("EXPLAIN (PLAN PARSED) SELECT COUNT(*) FROM test"), containsString("plan")); assertThat(readLine(), startsWith("----------")); assertThat(readLine(), startsWith("With[{}]")); - assertThat(readLine(), startsWith("\\_Project[[?COUNT[?*]]]")); + assertThat(readLine(), startsWith("\\_Project[[?COUNT[?*] AS ?]]")); assertThat(readLine(), startsWith(" \\_UnresolvedRelation[test]")); assertEquals("", readLine()); assertThat(command("EXPLAIN " + (randomBoolean() ? "" : "(PLAN ANALYZED) ") + "SELECT COUNT(*) FROM test"), containsString("plan")); assertThat(readLine(), startsWith("----------")); - assertThat(readLine(), startsWith("Aggregate[[],[Count[*=1")); + assertThat(readLine(), startsWith("Aggregate[[],[COUNT(*)")); assertThat(readLine(), startsWith("\\_EsRelation[test][i{f}#")); assertEquals("", readLine()); assertThat(command("EXPLAIN (PLAN OPTIMIZED) SELECT COUNT(*) FROM test"), containsString("plan")); assertThat(readLine(), startsWith("----------")); - assertThat(readLine(), startsWith("Aggregate[[],[Count[*=1")); + assertThat(readLine(), startsWith("Aggregate[[],[COUNT(*)")); assertThat(readLine(), startsWith("\\_EsRelation[test][i{f}#")); assertEquals("", readLine()); diff --git a/x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/rest/RestSqlTestCase.java b/x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/rest/RestSqlTestCase.java index a313ddc771bf9..3215c6b35efdf 100644 --- a/x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/rest/RestSqlTestCase.java +++ b/x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/rest/RestSqlTestCase.java @@ -422,36 +422,36 @@ public void testPrettyPrintingEnabled() throws IOException { boolean columnar = randomBoolean(); String expected = ""; if (columnar) { - expected = "{\n" + - " \"columns\" : [\n" + - " {\n" + - " \"name\" : \"test1\",\n" + - " \"type\" : \"text\"\n" + - " }\n" + - " ],\n" + - " \"values\" : [\n" + - " [\n" + - " \"test1\",\n" + - " \"test2\"\n" + - " ]\n" + - " ]\n" + + expected = "{\n" + + " \"columns\" : [\n" + + " {\n" + + " \"name\" : \"test1\",\n" + + " \"type\" : \"text\"\n" + + " }\n" + + " ],\n" + + " \"values\" : [\n" + + " [\n" + + " \"test1\",\n" + + " \"test2\"\n" + + " ]\n" + + " ]\n" + "}\n"; } else { - expected = "{\n" + - " \"columns\" : [\n" + - " {\n" + - " \"name\" : \"test1\",\n" + - " \"type\" : \"text\"\n" + - " }\n" + - " ],\n" + - " \"rows\" : [\n" + - " [\n" + - " \"test1\"\n" + - " ],\n" + - " [\n" + - " \"test2\"\n" + - " ]\n" + - " ]\n" + + expected = "{\n" + + " \"columns\" : [\n" + + " {\n" + + " \"name\" : \"test1\",\n" + + " \"type\" : \"text\"\n" + + " }\n" + + " ],\n" + + " \"rows\" : [\n" + + " [\n" + + " \"test1\"\n" + + " ],\n" + + " [\n" + + " \"test2\"\n" + + " ]\n" + + " ]\n" + "}\n"; } executeAndAssertPrettyPrinting(expected, "true", columnar); @@ -638,14 +638,14 @@ public void testTranslateQueryWithGroupByAndHaving() throws IOException { Map aggregations2 = (Map) groupby.get("aggregations"); assertEquals(2, aggregations2.size()); - List aggKeys = new ArrayList<>(2); + List aggKeys = new ArrayList<>(2); String aggFilterKey = null; for (Map.Entry entry : aggregations2.entrySet()) { String key = entry.getKey(); if (key.startsWith("having")) { aggFilterKey = key; } else { - aggKeys.add(Integer.valueOf(key)); + aggKeys.add(key); @SuppressWarnings("unchecked") Map aggr = (Map) entry.getValue(); assertEquals(1, aggr.size()); diff --git a/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec index 182b6c2c76f39..ed1ae60b14c85 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec @@ -604,9 +604,31 @@ SELECT COUNT(ALL first_name) all_names, COUNT(*) c FROM test_emp; all_names | c ---------------+--------------- -90 |100 +90 |100 ; +countDistinctAndLiteral +schema::ln:l|ccc:l +SELECT COUNT(last_name) ln, COUNT(*) ccc FROM test_emp GROUP BY gender HAVING ln>5 AND ccc>5; + + ln | ccc +---------------+------------- +10 |10 +33 |33 +57 |57 +; + +countSmallCountTypesWithHaving +schema::ln:l|dln:l|fn:l|dfn:l|ccc:l +SELECT COUNT(last_name) ln, COUNT(distinct last_name) dln, COUNT(first_name) fn, COUNT(distinct first_name) dfn, COUNT(*) ccc FROM test_emp GROUP BY gender HAVING dln>5 AND ln>32 AND dfn>1 AND fn>1 AND ccc>5; + + ln | dln | fn | dfn | ccc +---------------+-------------+---------------+------------+------------- +33 |32 |32 |32 |33 +57 |54 |48 |48 |57 +; + + countAllCountTypesWithHaving schema::ln:l|dln:l|fn:l|dfn:l|ccc:l SELECT COUNT(last_name) ln, COUNT(distinct last_name) dln, COUNT(first_name) fn, COUNT(distinct first_name) dfn, COUNT(*) ccc FROM test_emp GROUP BY gender HAVING dln>5 AND ln>32 AND dfn>1 AND fn>1 AND ccc>5; diff --git a/x-pack/plugin/sql/qa/src/main/resources/math.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/math.csv-spec index 372614dcb0052..a333de34987c1 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/math.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/math.csv-spec @@ -64,8 +64,8 @@ SELECT TRUNC(salary, 2) TRUNCATED, salary FROM test_emp GROUP BY TRUNCATED, sala truncateWithAsciiAndOrderBy SELECT TRUNCATE(ASCII(LEFT(first_name,1)), -1) AS initial, first_name, ASCII(LEFT(first_name, 1)) FROM test_emp ORDER BY ASCII(LEFT(first_name, 1)) DESC LIMIT 15; - initial | first_name |ASCII(LEFT(first_name,1)) ----------------+---------------+------------------------- + initial | first_name |ASCII(LEFT(first_name, 1)) +---------------+---------------+-------------------------- 90 |Zvonko |90 90 |Zhongwei |90 80 |Yongqiao |89 diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Analyzer.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Analyzer.java index c790626c5fbcb..030d059ccfbd2 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Analyzer.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Analyzer.java @@ -19,6 +19,7 @@ import org.elasticsearch.xpack.sql.expression.Foldables; import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.expression.Order; +import org.elasticsearch.xpack.sql.expression.ReferenceAttribute; import org.elasticsearch.xpack.sql.expression.SubQueryExpression; import org.elasticsearch.xpack.sql.expression.UnresolvedAlias; import org.elasticsearch.xpack.sql.expression.UnresolvedAttribute; @@ -28,10 +29,8 @@ import org.elasticsearch.xpack.sql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.sql.expression.function.Functions; import org.elasticsearch.xpack.sql.expression.function.UnresolvedFunction; -import org.elasticsearch.xpack.sql.expression.function.aggregate.Count; import org.elasticsearch.xpack.sql.expression.function.scalar.Cast; import org.elasticsearch.xpack.sql.expression.predicate.operator.arithmetic.ArithmeticOperation; -import org.elasticsearch.xpack.sql.expression.predicate.regex.RegexMatch; import org.elasticsearch.xpack.sql.plan.TableIdentifier; import org.elasticsearch.xpack.sql.plan.logical.Aggregate; import org.elasticsearch.xpack.sql.plan.logical.EsRelation; @@ -66,7 +65,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; +import java.util.stream.Stream; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -329,12 +328,13 @@ else if (plan instanceof Aggregate) { return new Aggregate(a.source(), a.child(), a.groupings(), expandProjections(a.aggregates(), a.child())); } - // if the grouping is unresolved but the aggs are, use the latter to resolve the former + // if the grouping is unresolved but the aggs are, use the former to resolve the latter // solves the case of queries declaring an alias in SELECT and referring to it in GROUP BY + // e.g. SELECT x AS a ... GROUP BY a if (!a.expressionsResolved() && Resolvables.resolved(a.aggregates())) { List groupings = a.groupings(); List newGroupings = new ArrayList<>(); - AttributeMap resolved = Expressions.asAttributeMap(a.aggregates()); + AttributeMap resolved = Expressions.aliases(a.aggregates()); boolean changed = false; for (Expression grouping : groupings) { if (grouping instanceof UnresolvedAttribute) { @@ -363,9 +363,10 @@ else if (plan instanceof Join) { else if (plan instanceof OrderBy) { OrderBy o = (OrderBy) plan; if (!o.resolved()) { - List resolvedOrder = o.order().stream() - .map(or -> resolveExpression(or, o.child())) - .collect(toList()); + List resolvedOrder = new ArrayList<>(o.order().size()); + for (Order order : o.order()) { + resolvedOrder.add(resolveExpression(order, o.child())); + } return new OrderBy(o.source(), o.child(), resolvedOrder); } } @@ -606,19 +607,53 @@ protected LogicalPlan rule(LogicalPlan plan) { if (plan instanceof OrderBy && !plan.resolved() && plan.childrenResolved()) { OrderBy o = (OrderBy) plan; - List maybeResolved = o.order().stream() - .map(or -> tryResolveExpression(or, o.child())) - .collect(toList()); - + LogicalPlan child = o.child(); + List maybeResolved = new ArrayList<>(); + for (Order or : o.order()) { + maybeResolved.add(or.resolved() ? or : tryResolveExpression(or, child)); + } + + Stream referencesStream = maybeResolved.stream() + .filter(Expression::resolved); + + // if there are any references in the output + // try and resolve them to the source in order to compare the source expressions + // e.g. ORDER BY a + 1 + // \ SELECT a + 1 + // a + 1 in SELECT is actually Alias("a + 1", a + 1) and translates to ReferenceAttribute + // in the output. However it won't match the unnamed a + 1 despite being the same expression + // so explicitly compare the source + + // if there's a match, remove the item from the reference stream + if (Expressions.hasReferenceAttribute(child.outputSet())) { + final Map collectRefs = new LinkedHashMap<>(); + + // collect aliases + child.forEachUp(p -> p.forEachExpressionsUp(e -> { + if (e instanceof Alias) { + Alias a = (Alias) e; + collectRefs.put(a.toAttribute(), a.child()); + } + })); + + referencesStream = referencesStream.filter(r -> { + for (Attribute attr : child.outputSet()) { + if (attr instanceof ReferenceAttribute) { + Expression source = collectRefs.getOrDefault(attr, attr); + // found a match, no need to resolve it further + // so filter it out + if (source.equals(r.child())) { + return false; + } + } + } + return true; + }); + } - Set resolvedRefs = maybeResolved.stream() - .filter(Expression::resolved) - .collect(Collectors.toSet()); + AttributeSet resolvedRefs = Expressions.references(referencesStream.collect(toList())); - AttributeSet missing = Expressions.filterReferences( - resolvedRefs, - o.child().outputSet() - ); + AttributeSet missing = resolvedRefs.subtract(child.outputSet()); if (!missing.isEmpty()) { // Add missing attributes but project them away afterwards @@ -650,6 +685,7 @@ protected LogicalPlan rule(LogicalPlan plan) { if (plan instanceof Filter && !plan.resolved() && plan.childrenResolved()) { Filter f = (Filter) plan; Expression maybeResolved = tryResolveExpression(f.condition(), f.child()); + AttributeSet resolvedRefs = new AttributeSet(maybeResolved.references().stream() .filter(Expression::resolved) .collect(toList())); @@ -708,9 +744,11 @@ private static LogicalPlan propagateMissing(LogicalPlan plan, AttributeSet missi if (plan instanceof Aggregate) { Aggregate a = (Aggregate) plan; // missing attributes can only be grouping expressions + // however take into account aliased groups + // SELECT x AS i ... GROUP BY i for (Attribute m : missing) { - // but we don't can't add an agg if the group is missing - if (!Expressions.anyMatch(a.groupings(), m::semanticEquals)) { + // but we can't add an agg if the group is missing + if (!Expressions.match(a.groupings(), m::semanticEquals)) { if (m instanceof Attribute) { // pass failure information to help the verifier m = new UnresolvedAttribute(m.source(), m.name(), m.qualifier(), null, null, @@ -758,7 +796,7 @@ private static UnresolvedAttribute resolveMetadataToMessage(UnresolvedAttribute // SELECT int AS i FROM t WHERE i > 10 // // As such, identify all project and aggregates that have a Filter child - // and look at any resoled aliases that match and replace them. + // and look at any resolved aliases that match and replace them. private class ResolveFilterRefs extends AnalyzeRule { @Override @@ -815,49 +853,10 @@ private Expression replaceAliases(Expression condition, List { @Override protected LogicalPlan rule(LogicalPlan plan) { - Map> seen = new LinkedHashMap<>(); - // collect (and replace duplicates) - LogicalPlan p = plan.transformExpressionsUp(e -> collectResolvedAndReplace(e, seen)); - // resolve based on seen - return resolve(p, seen); - } - - private Expression collectResolvedAndReplace(Expression e, Map> seen) { - if (e instanceof Function && e.resolved()) { - Function f = (Function) e; - String fName = f.functionName(); - // the function is resolved and its name normalized already - List list = getList(seen, fName); - for (Function seenFunction : list) { - if (seenFunction != f && f.arguments().equals(seenFunction.arguments())) { - // TODO: we should move to always compare the functions directly - // Special check for COUNT: an already seen COUNT function will be returned only if its DISTINCT property - // matches the one from the unresolved function to be checked. - // Same for LIKE/RLIKE: the equals function also compares the pattern of LIKE/RLIKE - if (seenFunction instanceof Count || seenFunction instanceof RegexMatch) { - if (seenFunction.equals(f)){ - return seenFunction; - } - } else { - return seenFunction; - } - } - } - list.add(f); - } - - return e; - } - - protected LogicalPlan resolve(LogicalPlan plan, Map> seen) { return plan.transformExpressionsUp(e -> { if (e instanceof UnresolvedFunction) { UnresolvedFunction uf = (UnresolvedFunction) e; @@ -880,48 +879,17 @@ protected LogicalPlan resolve(LogicalPlan plan, Map> seen } String functionName = functionRegistry.resolveAlias(name); - - List list = getList(seen, functionName); - // first try to resolve from seen functions - if (!list.isEmpty()) { - for (Function seenFunction : list) { - if (uf.arguments().equals(seenFunction.arguments())) { - // Special check for COUNT: an already seen COUNT function will be returned only if its DISTINCT property - // matches the one from the unresolved function to be checked. - if (seenFunction instanceof Count) { - if (uf.sameAs((Count) seenFunction)) { - return seenFunction; - } - } else { - return seenFunction; - } - } - } - } - - // not seen before, use the registry - if (!functionRegistry.functionExists(functionName)) { + if (functionRegistry.functionExists(functionName) == false) { return uf.missing(functionName, functionRegistry.listFunctions()); } // TODO: look into Generator for significant terms, etc.. FunctionDefinition def = functionRegistry.resolveFunction(functionName); Function f = uf.buildResolved(configuration, def); - - list.add(f); return f; } return e; }); } - - private List getList(Map> seen, String name) { - List list = seen.get(name); - if (list == null) { - list = new ArrayList<>(); - seen.put(name, list); - } - return list; - } } private static class ResolveAliases extends AnalyzeRule { @@ -1103,11 +1071,12 @@ private Set findMissingAggregate(Aggregate target, Expression f Set missing = new LinkedHashSet<>(); for (Expression filterAgg : from.collect(Functions::isAggregate)) { - if (!Expressions.anyMatch(target.aggregates(), - a -> { - Attribute attr = Expressions.attribute(a); - return attr != null && attr.semanticEquals(Expressions.attribute(filterAgg)); - })) { + if (Expressions.anyMatch(target.aggregates(), a -> { + if (a instanceof Alias) { + a = ((Alias) a).child(); + } + return a.equals(filterAgg); + }) == false) { missing.add(Expressions.wrapAsNamed(filterAgg)); } } @@ -1135,10 +1104,10 @@ protected LogicalPlan rule(OrderBy ob) { List orders = ob.order(); // 1. collect aggs inside an order by - List aggs = new ArrayList<>(); + List aggs = new ArrayList<>(); for (Order order : orders) { if (Functions.isAggregate(order.child())) { - aggs.add(Expressions.wrapAsNamed(order.child())); + aggs.add(order.child()); } } if (aggs.isEmpty()) { @@ -1154,9 +1123,14 @@ protected LogicalPlan rule(OrderBy ob) { List missing = new ArrayList<>(); - for (NamedExpression orderedAgg : aggs) { - if (Expressions.anyMatch(a.aggregates(), e -> Expressions.equalsAsAttribute(e, orderedAgg)) == false) { - missing.add(orderedAgg); + for (Expression orderedAgg : aggs) { + if (Expressions.anyMatch(a.aggregates(), e -> { + if (e instanceof Alias) { + e = ((Alias) e).child(); + } + return e.equals(orderedAgg); + }) == false) { + missing.add(Expressions.wrapAsNamed(orderedAgg)); } } // agg already contains all aggs @@ -1176,39 +1150,6 @@ protected LogicalPlan rule(OrderBy ob) { } } - private class PruneDuplicateFunctions extends AnalyzeRule { - - @Override - protected boolean skipResolved() { - return false; - } - - @Override - public LogicalPlan rule(LogicalPlan plan) { - List seen = new ArrayList<>(); - LogicalPlan p = plan.transformExpressionsUp(e -> rule(e, seen)); - return p; - } - - private Expression rule(Expression e, List seen) { - if (e instanceof Function) { - Function f = (Function) e; - for (Function seenFunction : seen) { - if (seenFunction != f && functionsEquals(f, seenFunction)) { - return seenFunction; - } - } - seen.add(f); - } - - return e; - } - - private boolean functionsEquals(Function f, Function seenFunction) { - return f.sourceText().equals(seenFunction.sourceText()) && f.arguments().equals(seenFunction.arguments()); - } - } - private class ImplicitCasting extends AnalyzeRule { @Override @@ -1282,7 +1223,7 @@ protected LogicalPlan rule(LogicalPlan plan) { if (plan instanceof Aggregate) { Aggregate a = (Aggregate) plan; - // aliases inside GROUP BY are irellevant so remove all of them + // aliases inside GROUP BY are irrelevant so remove all of them // however aggregations are important (ultimately a projection) return new Aggregate(a.source(), a.child(), cleanAllAliases(a.groupings()), cleanChildrenAliases(a.aggregates())); } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Verifier.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Verifier.java index 3f5caa064a2ed..34def1238d00f 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Verifier.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Verifier.java @@ -8,6 +8,7 @@ import org.elasticsearch.xpack.sql.capabilities.Unresolvable; import org.elasticsearch.xpack.sql.expression.Alias; import org.elasticsearch.xpack.sql.expression.Attribute; +import org.elasticsearch.xpack.sql.expression.AttributeMap; import org.elasticsearch.xpack.sql.expression.AttributeSet; import org.elasticsearch.xpack.sql.expression.Exists; import org.elasticsearch.xpack.sql.expression.Expression; @@ -15,18 +16,16 @@ import org.elasticsearch.xpack.sql.expression.FieldAttribute; import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.NamedExpression; +import org.elasticsearch.xpack.sql.expression.ReferenceAttribute; import org.elasticsearch.xpack.sql.expression.UnresolvedAttribute; import org.elasticsearch.xpack.sql.expression.function.Function; -import org.elasticsearch.xpack.sql.expression.function.FunctionAttribute; import org.elasticsearch.xpack.sql.expression.function.Functions; import org.elasticsearch.xpack.sql.expression.function.Score; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; -import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; import org.elasticsearch.xpack.sql.expression.function.aggregate.Max; import org.elasticsearch.xpack.sql.expression.function.aggregate.Min; import org.elasticsearch.xpack.sql.expression.function.aggregate.TopHits; import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunction; -import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunctionAttribute; import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.sql.plan.logical.Aggregate; import org.elasticsearch.xpack.sql.plan.logical.Distinct; @@ -215,8 +214,19 @@ Collection verify(LogicalPlan plan) { // if there are no (major) unresolved failures, do more in-depth analysis if (failures.isEmpty()) { - // collect Function to better reason about encountered attributes - Map resolvedFunctions = Functions.collectFunctions(plan); + final Map collectRefs = new LinkedHashMap<>(); + + // collect Attribute sources + // only Aliases are interesting since these are the only ones that hide expressions + // FieldAttribute for example are self replicating. + plan.forEachUp(p -> p.forEachExpressionsUp(e -> { + if (e instanceof Alias) { + Alias a = (Alias) e; + collectRefs.put(a.toAttribute(), a.child()); + } + })); + + AttributeMap attributeRefs = new AttributeMap<>(collectRefs); // for filtering out duplicated errors final Set groupingFailures = new LinkedHashSet<>(); @@ -234,17 +244,17 @@ Collection verify(LogicalPlan plan) { Set localFailures = new LinkedHashSet<>(); checkGroupingFunctionInGroupBy(p, localFailures); - checkFilterOnAggs(p, localFailures); - checkFilterOnGrouping(p, localFailures); + checkFilterOnAggs(p, localFailures, attributeRefs); + checkFilterOnGrouping(p, localFailures, attributeRefs); - if (!groupingFailures.contains(p)) { - checkGroupBy(p, localFailures, resolvedFunctions, groupingFailures); + if (groupingFailures.contains(p) == false) { + checkGroupBy(p, localFailures, attributeRefs, groupingFailures); } checkForScoreInsideFunctions(p, localFailures); checkNestedUsedInGroupByOrHaving(p, localFailures); checkForGeoFunctionsOnDocValues(p, localFailures); - checkPivot(p, localFailures); + checkPivot(p, localFailures, attributeRefs); // everything checks out // mark the plan as analyzed @@ -297,17 +307,18 @@ Collection verify(LogicalPlan plan) { * 2a. HAVING also requires an Aggregate function * 3. composite agg (used for GROUP BY) allows ordering only on the group keys */ - private static boolean checkGroupBy(LogicalPlan p, Set localFailures, - Map resolvedFunctions, Set groupingFailures) { + private static boolean checkGroupBy(LogicalPlan p, Set localFailures, AttributeMap attributeRefs, + Set groupingFailures) { return checkGroupByInexactField(p, localFailures) - && checkGroupByAgg(p, localFailures, resolvedFunctions) - && checkGroupByOrder(p, localFailures, groupingFailures) - && checkGroupByHaving(p, localFailures, groupingFailures, resolvedFunctions) + && checkGroupByAgg(p, localFailures, attributeRefs) + && checkGroupByOrder(p, localFailures, groupingFailures, attributeRefs) + && checkGroupByHaving(p, localFailures, groupingFailures, attributeRefs) && checkGroupByTime(p, localFailures); } // check whether an orderBy failed or if it occurs on a non-key - private static boolean checkGroupByOrder(LogicalPlan p, Set localFailures, Set groupingFailures) { + private static boolean checkGroupByOrder(LogicalPlan p, Set localFailures, Set groupingFailures, + AttributeMap attributeRefs) { if (p instanceof OrderBy) { OrderBy o = (OrderBy) p; LogicalPlan child = o.child(); @@ -328,7 +339,7 @@ private static boolean checkGroupByOrder(LogicalPlan p, Set localFailur Expression e = oe.child(); // aggregates are allowed - if (Functions.isAggregate(e) || e instanceof AggregateFunctionAttribute) { + if (Functions.isAggregate(attributeRefs.getOrDefault(e, e))) { return; } @@ -375,7 +386,7 @@ private static boolean checkGroupByOrder(LogicalPlan p, Set localFailur } private static boolean checkGroupByHaving(LogicalPlan p, Set localFailures, - Set groupingFailures, Map functions) { + Set groupingFailures, AttributeMap attributeRefs) { if (p instanceof Filter) { Filter f = (Filter) p; if (f.child() instanceof Aggregate) { @@ -385,7 +396,7 @@ private static boolean checkGroupByHaving(LogicalPlan p, Set localFailu Set unsupported = new LinkedHashSet<>(); Expression condition = f.condition(); // variation of checkGroupMatch customized for HAVING, which requires just aggregations - condition.collectFirstChildren(c -> checkGroupByHavingHasOnlyAggs(c, missing, unsupported, functions)); + condition.collectFirstChildren(c -> checkGroupByHavingHasOnlyAggs(c, missing, unsupported, attributeRefs)); if (!missing.isEmpty()) { String plural = missing.size() > 1 ? "s" : StringUtils.EMPTY; @@ -411,17 +422,11 @@ private static boolean checkGroupByHaving(LogicalPlan p, Set localFailu private static boolean checkGroupByHavingHasOnlyAggs(Expression e, Set missing, - Set unsupported, Map functions) { + Set unsupported, AttributeMap attributeRefs) { // resolve FunctionAttribute to backing functions - if (e instanceof FunctionAttribute) { - FunctionAttribute fa = (FunctionAttribute) e; - Function function = functions.get(fa.functionId()); - // TODO: this should be handled by a different rule - if (function == null) { - return false; - } - e = function; + if (e instanceof ReferenceAttribute) { + e = attributeRefs.get(e); } // scalar functions can be a binary tree @@ -432,7 +437,7 @@ private static boolean checkGroupByHavingHasOnlyAggs(Expression e, Set checkGroupByHavingHasOnlyAggs(c, missing, unsupported, functions)); + arg.collectFirstChildren(c -> checkGroupByHavingHasOnlyAggs(c, missing, unsupported, attributeRefs)); } return true; @@ -449,7 +454,7 @@ private static boolean checkGroupByHavingHasOnlyAggs(Expression e, Set expressions, Set onlyExact = new Holder<>(Boolean.TRUE); expressions.forEach(e -> e.forEachUp(c -> { - EsField.Exact exact = c.getExactInfo(); - if (exact.hasExact() == false) { + EsField.Exact exact = c.getExactInfo(); + if (exact.hasExact() == false) { localFailures.add(fail(c, "Field [{}] of data type [{}] cannot be used for grouping; {}", c.sourceText(), c.dataType().typeName, exact.errorMsg())); onlyExact.set(Boolean.FALSE); - } - }, FieldAttribute.class)); + } + }, FieldAttribute.class)); return onlyExact.get(); } - private static boolean onlyRawFields(Iterable expressions, Set localFailures) { + private static boolean onlyRawFields(Iterable expressions, Set localFailures, + AttributeMap attributeRefs) { Holder onlyExact = new Holder<>(Boolean.TRUE); expressions.forEach(e -> e.forEachDown(c -> { - if (c instanceof Function || c instanceof FunctionAttribute) { + if (c instanceof ReferenceAttribute) { + c = attributeRefs.getOrDefault(c, c); + } + if (c instanceof Function) { localFailures.add(fail(c, "No functions allowed (yet); encountered [{}]", c.sourceText())); onlyExact.set(Boolean.FALSE); } @@ -522,7 +531,7 @@ private static boolean checkGroupByTime(LogicalPlan p, Set localFailure } // check whether plain columns specified in an agg are mentioned in the group-by - private static boolean checkGroupByAgg(LogicalPlan p, Set localFailures, Map functions) { + private static boolean checkGroupByAgg(LogicalPlan p, Set localFailures, AttributeMap attributeRefs) { if (p instanceof Aggregate) { Aggregate a = (Aggregate) p; @@ -566,7 +575,7 @@ private static boolean checkGroupByAgg(LogicalPlan p, Set localFailures Map> missing = new LinkedHashMap<>(); a.aggregates().forEach(ne -> - ne.collectFirstChildren(c -> checkGroupMatch(c, ne, a.groupings(), missing, functions))); + ne.collectFirstChildren(c -> checkGroupMatch(c, ne, a.groupings(), missing, attributeRefs))); if (!missing.isEmpty()) { String plural = missing.size() > 1 ? "s" : StringUtils.EMPTY; @@ -581,23 +590,16 @@ private static boolean checkGroupByAgg(LogicalPlan p, Set localFailures } private static boolean checkGroupMatch(Expression e, Node source, List groupings, - Map> missing, Map functions) { + Map> missing, AttributeMap attributeRefs) { // 1:1 match if (Expressions.match(groupings, e::semanticEquals)) { return true; } - // resolve FunctionAttribute to backing functions - if (e instanceof FunctionAttribute) { - FunctionAttribute fa = (FunctionAttribute) e; - Function function = functions.get(fa.functionId()); - // TODO: this should be handled by a different rule - if (function == null) { - return false; - } - e = function; + if (e instanceof ReferenceAttribute) { + e = attributeRefs.get(e); } // scalar functions can be a binary tree @@ -613,7 +615,7 @@ private static boolean checkGroupMatch(Expression e, Node source, List checkGroupMatch(c, source, groupings, missing, functions)); + arg.collectFirstChildren(c -> checkGroupMatch(c, source, groupings, missing, attributeRefs)); } return true; @@ -658,7 +660,7 @@ else if (p instanceof Aggregate) { Aggregate a = (Aggregate) p; a.aggregates().forEach(agg -> agg.forEachDown(e -> { if (a.groupings().size() == 0 - || Expressions.anyMatch(a.groupings(), g -> g instanceof Function && e.functionEquals((Function) g)) == false) { + || Expressions.anyMatch(a.groupings(), g -> g instanceof Function && e.equals(g)) == false) { localFailures.add(fail(e, "[{}] needs to be part of the grouping", Expressions.name(e))); } else { @@ -681,12 +683,12 @@ private static void checkGroupingFunctionTarget(GroupingFunction f, Set }); } - private static void checkFilterOnAggs(LogicalPlan p, Set localFailures) { + private static void checkFilterOnAggs(LogicalPlan p, Set localFailures, AttributeMap attributeRefs) { if (p instanceof Filter) { Filter filter = (Filter) p; if ((filter.child() instanceof Aggregate) == false) { filter.condition().forEachDown(e -> { - if (Functions.isAggregate(e) || e instanceof AggregateFunctionAttribute) { + if (Functions.isAggregate(attributeRefs.getOrDefault(e, e)) == true) { localFailures.add( fail(e, "Cannot use WHERE filtering on aggregate function [{}], use HAVING instead", Expressions.name(e))); } @@ -696,11 +698,11 @@ private static void checkFilterOnAggs(LogicalPlan p, Set localFailures) } - private static void checkFilterOnGrouping(LogicalPlan p, Set localFailures) { + private static void checkFilterOnGrouping(LogicalPlan p, Set localFailures, AttributeMap attributeRefs) { if (p instanceof Filter) { Filter filter = (Filter) p; filter.condition().forEachDown(e -> { - if (Functions.isGrouping(e) || e instanceof GroupingFunctionAttribute) { + if (Functions.isGrouping(attributeRefs.getOrDefault(e, e))) { localFailures .add(fail(e, "Cannot filter on grouping function [{}], use its argument instead", Expressions.name(e))); } @@ -787,11 +789,11 @@ private static void checkForGeoFunctionsOnDocValues(LogicalPlan p, Set }, FieldAttribute.class)), OrderBy.class); } - private static void checkPivot(LogicalPlan p, Set localFailures) { + private static void checkPivot(LogicalPlan p, Set localFailures, AttributeMap attributeRefs) { p.forEachDown(pv -> { // check only exact fields are used inside PIVOTing if (onlyExactFields(combine(pv.groupingSet(), pv.column()), localFailures) == false - || onlyRawFields(pv.groupingSet(), localFailures) == false) { + || onlyRawFields(pv.groupingSet(), localFailures, attributeRefs) == false) { // if that is not the case, no need to do further validation since the declaration is fundamentally wrong return; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java index 6b8bf7ab6df96..d8cf81b539427 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java @@ -39,7 +39,6 @@ import org.elasticsearch.xpack.sql.execution.search.extractor.PivotExtractor; import org.elasticsearch.xpack.sql.execution.search.extractor.TopHitsAggExtractor; import org.elasticsearch.xpack.sql.expression.Attribute; -import org.elasticsearch.xpack.sql.expression.ExpressionId; import org.elasticsearch.xpack.sql.expression.gen.pipeline.AggExtractorInput; import org.elasticsearch.xpack.sql.expression.gen.pipeline.AggPathInput; import org.elasticsearch.xpack.sql.expression.gen.pipeline.HitExtractorInput; @@ -378,11 +377,11 @@ abstract static class BaseAggActionListener extends BaseActionListener { protected List initBucketExtractors(SearchResponse response) { // create response extractors for the first time - List> refs = query.fields(); + List> refs = query.fields(); List exts = new ArrayList<>(refs.size()); ConstantExtractor totalCount = new ConstantExtractor(response.getHits().getTotalHits().value); - for (Tuple ref : refs) { + for (Tuple ref : refs) { exts.add(createExtractor(ref.v1(), totalCount)); } return exts; @@ -448,10 +447,10 @@ static class ScrollActionListener extends BaseActionListener { @Override protected void handleResponse(SearchResponse response, ActionListener listener) { // create response extractors for the first time - List> refs = query.fields(); + List> refs = query.fields(); List exts = new ArrayList<>(refs.size()); - for (Tuple ref : refs) { + for (Tuple ref : refs) { exts.add(createExtractor(ref.v1())); } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Alias.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Alias.java index 4ebc030c281d2..ef8611b49690f 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Alias.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Alias.java @@ -5,14 +5,10 @@ */ package org.elasticsearch.xpack.sql.expression; -import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; -import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; import org.elasticsearch.xpack.sql.tree.NodeInfo; import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.DataType; -import org.elasticsearch.xpack.sql.type.EsField; -import java.util.Collections; import java.util.List; import static java.util.Collections.singletonList; @@ -44,11 +40,11 @@ public Alias(Source source, String name, String qualifier, Expression child) { this(source, name, qualifier, child, null); } - public Alias(Source source, String name, String qualifier, Expression child, ExpressionId id) { + public Alias(Source source, String name, String qualifier, Expression child, NameId id) { this(source, name, qualifier, child, id, false); } - public Alias(Source source, String name, String qualifier, Expression child, ExpressionId id, boolean synthetic) { + public Alias(Source source, String name, String qualifier, Expression child, NameId id, boolean synthetic) { super(source, name, singletonList(child), id, synthetic); this.child = child; this.qualifier = qualifier; @@ -92,35 +88,13 @@ public DataType dataType() { @Override public Attribute toAttribute() { if (lazyAttribute == null) { - lazyAttribute = createAttribute(); + lazyAttribute = resolved() == true ? + new ReferenceAttribute(source(), name(), dataType(), qualifier, nullable(), id(), synthetic()) : + new UnresolvedAttribute(source(), name(), qualifier); } return lazyAttribute; } - @Override - public ScriptTemplate asScript() { - throw new SqlIllegalArgumentException("Encountered a bug; an alias should never be scripted"); - } - - private Attribute createAttribute() { - if (resolved()) { - Expression c = child(); - - Attribute attr = Expressions.attribute(c); - if (attr != null) { - return attr.clone(source(), name(), child.dataType(), qualifier, child.nullable(), id(), synthetic()); - } - else { - // TODO: WE need to fix this fake Field - return new FieldAttribute(source(), null, name(), - new EsField(name(), child.dataType(), Collections.emptyMap(), true), - qualifier, child.nullable(), id(), synthetic()); - } - } - - return new UnresolvedAttribute(source(), name(), qualifier); - } - @Override public String toString() { return child + " AS " + name() + "#" + id(); diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Attribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Attribute.java index 9f6b54badaf20..bda8287115e0f 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Attribute.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Attribute.java @@ -5,9 +5,6 @@ */ package org.elasticsearch.xpack.sql.expression; -import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; -import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; -import org.elasticsearch.xpack.sql.tree.NodeInfo; import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.DataType; @@ -17,24 +14,16 @@ import static java.util.Collections.emptyList; /** - * {@link Expression}s that can be materialized and represent the result columns sent to the client. - * Typically are converted into constants, functions or Elasticsearch order-bys, - * aggregations, or queries. They can also be extracted from the result of a search. - * + * {@link Expression}s that can be materialized and describe properties of the derived table. + * In other words, an attribute represent a column in the results of a query. + * * In the statement {@code SELECT ABS(foo), A, B+C FROM ...} the three named * expressions {@code ABS(foo), A, B+C} get converted to attributes and the user can * only see Attributes. * - * In the statement {@code SELECT foo FROM TABLE WHERE foo > 10 + 1} both {@code foo} and - * {@code 10 + 1} are named expressions, the first due to the SELECT, the second due to being a function. - * However since {@code 10 + 1} is used for filtering it doesn't appear appear in the result set - * (derived table) and as such it is never translated to an attribute. - * "foo" on the other hand is since it's a column in the result set. - * - * Another example {@code SELECT foo FROM ... WHERE bar > 10 +1} {@code foo} gets - * converted into an Attribute, bar does not. That's because {@code bar} is used for - * filtering alone but it's not part of the projection meaning the user doesn't - * need it in the derived table. + * In the statement {@code SELECT foo FROM TABLE WHERE foo > 10 + 1} only {@code foo} inside the SELECT + * is a named expression (an {@code Alias} will be created automatically for it). + * The rest are not as they are not part of the projection and thus are not part of the derived table. */ public abstract class Attribute extends NamedExpression { @@ -45,15 +34,15 @@ public abstract class Attribute extends NamedExpression { // can the attr be null - typically used in JOINs private final Nullability nullability; - public Attribute(Source source, String name, String qualifier, ExpressionId id) { + public Attribute(Source source, String name, String qualifier, NameId id) { this(source, name, qualifier, Nullability.TRUE, id); } - public Attribute(Source source, String name, String qualifier, Nullability nullability, ExpressionId id) { + public Attribute(Source source, String name, String qualifier, Nullability nullability, NameId id) { this(source, name, qualifier, nullability, id, false); } - public Attribute(Source source, String name, String qualifier, Nullability nullability, ExpressionId id, boolean synthetic) { + public Attribute(Source source, String name, String qualifier, Nullability nullability, NameId id, boolean synthetic) { super(source, name, emptyList(), id, synthetic); this.qualifier = qualifier; this.nullability = nullability; @@ -64,11 +53,6 @@ public final Expression replaceChildren(List newChildren) { throw new UnsupportedOperationException("this type of node doesn't have any children to replace"); } - @Override - public ScriptTemplate asScript() { - throw new SqlIllegalArgumentException("Encountered a bug - an attribute should never be scripted"); - } - public String qualifier() { return qualifier; } @@ -105,16 +89,16 @@ public Attribute withNullability(Nullability nullability) { synthetic()); } - public Attribute withDataType(DataType type) { - return Objects.equals(dataType(), type) ? this : clone(source(), name(), type, qualifier(), nullable(), id(), synthetic()); + public Attribute withId(NameId id) { + return clone(source(), name(), dataType(), qualifier(), nullable(), id, synthetic()); } - public Attribute withId(ExpressionId id) { - return clone(source(), name(), dataType(), qualifier(), nullable(), id, synthetic()); + public Attribute withDataType(DataType type) { + return Objects.equals(dataType(), type) ? this : clone(source(), name(), type, qualifier(), nullable(), id(), synthetic()); } protected abstract Attribute clone(Source source, String name, DataType type, String qualifier, Nullability nullability, - ExpressionId id, boolean synthetic); + NameId id, boolean synthetic); @Override public Attribute toAttribute() { @@ -126,11 +110,6 @@ public int semanticHash() { return id().hashCode(); } - @Override - protected NodeInfo info() { - return null; - } - @Override public boolean semanticEquals(Expression other) { return other instanceof Attribute ? id().equals(((Attribute) other).id()) : false; @@ -154,7 +133,12 @@ public boolean equals(Object obj) { @Override public String toString() { - return name() + "{" + label() + "}" + "#" + id(); + return qualifiedName() + "{" + label() + "}" + "#" + id(); + } + + @Override + public String nodeString() { + return toString(); } protected abstract String label(); diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/AttributeMap.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/AttributeMap.java index bb8d373f98bfd..b513671a21d37 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/AttributeMap.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/AttributeMap.java @@ -32,14 +32,14 @@ static class AttributeWrapper { @Override public int hashCode() { - return attr.hashCode(); + return attr.semanticHash(); } @Override public boolean equals(Object obj) { if (obj instanceof AttributeWrapper) { AttributeWrapper aw = (AttributeWrapper) obj; - return attr.equals(aw.attr); + return attr.semanticEquals(aw.attr); } return false; @@ -63,7 +63,7 @@ private abstract static class UnwrappingSet extends AbstractSet { @Override public Iterator iterator() { - return new Iterator() { + return new Iterator<>() { final Iterator i = set.iterator(); @Override @@ -300,7 +300,7 @@ public void clear() { @Override public Set keySet() { if (keySet == null) { - keySet = new UnwrappingSet(delegate.keySet()) { + keySet = new UnwrappingSet<>(delegate.keySet()) { @Override protected Attribute unwrap(AttributeWrapper next) { return next.attr; @@ -321,10 +321,10 @@ public Collection values() { @Override public Set> entrySet() { if (entrySet == null) { - entrySet = new UnwrappingSet, Entry>(delegate.entrySet()) { + entrySet = new UnwrappingSet<>(delegate.entrySet()) { @Override protected Entry unwrap(final Entry next) { - return new Entry() { + return new Entry<>() { @Override public Attribute getKey() { return next.getKey().attr; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Exists.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Exists.java index 2363b52316c2f..d481d8e115fd3 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Exists.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Exists.java @@ -16,7 +16,7 @@ public Exists(Source source, LogicalPlan query) { this(source, query, null); } - public Exists(Source source, LogicalPlan query, ExpressionId id) { + public Exists(Source source, LogicalPlan query, NameId id) { super(source, query, id); } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expression.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expression.java index 2dde7e5f97d61..e2e3f99ca8790 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expression.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expression.java @@ -128,6 +128,11 @@ public boolean resolved() { @Override public String toString() { - return nodeName() + "[" + propertiesToString(false) + "]"; + return sourceText(); } -} + + @Override + public String propertiesToString(boolean skipIfChild) { + return super.propertiesToString(false); + } +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expressions.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expressions.java index 3e5450f01ac30..92703f4768f70 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expressions.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expressions.java @@ -6,17 +6,21 @@ package org.elasticsearch.xpack.sql.expression; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.Function; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.AttributeInput; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.ConstantInput; import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; import org.elasticsearch.xpack.sql.type.DataType; import org.elasticsearch.xpack.sql.type.DataTypes; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Predicate; -import java.util.stream.Collectors; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; @@ -103,33 +107,8 @@ public static AttributeSet references(List exps) { return set; } - public static AttributeSet filterReferences(Set exps, AttributeSet excluded) { - AttributeSet ret = new AttributeSet(); - while (exps.size() > 0) { - - Set filteredExps = new LinkedHashSet<>(); - for (Expression exp : exps) { - Expression attr = Expressions.attribute(exp); - if (attr == null || (excluded.contains(attr) == false)) { - filteredExps.add(exp); - } - } - - ret.addAll(new AttributeSet( - filteredExps.stream().filter(c->c.children().isEmpty()) - .flatMap(exp->exp.references().stream()) - .collect(Collectors.toSet()) - )); - - exps = filteredExps.stream() - .flatMap((Expression exp)->exp.children().stream()) - .collect(Collectors.toSet()); - } - return ret; - } - public static String name(Expression e) { - return e instanceof NamedExpression ? ((NamedExpression) e).name() : e.nodeName(); + return e instanceof NamedExpression ? ((NamedExpression) e).name() : e.sourceText(); } public static boolean isNull(Expression e) { @@ -149,9 +128,6 @@ public static Attribute attribute(Expression e) { if (e instanceof NamedExpression) { return ((NamedExpression) e).toAttribute(); } - if (e != null && e.foldable()) { - return Literal.of(e).toAttribute(); - } return null; } @@ -163,6 +139,25 @@ public static boolean equalsAsAttribute(Expression left, Expression right) { return true; } + public static AttributeMap aliases(List named) { + Map aliasMap = new LinkedHashMap<>(); + for (NamedExpression ne : named) { + if (ne instanceof Alias) { + aliasMap.put(ne.toAttribute(), ((Alias) ne).child()); + } + } + return new AttributeMap<>(aliasMap); + } + + public static boolean hasReferenceAttribute(Collection output) { + for (Attribute attribute : output) { + if (attribute instanceof ReferenceAttribute) { + return true; + } + } + return false; + } + public static List onlyPrimitiveFieldAttributes(Collection attributes) { List filtered = new ArrayList<>(); // add only primitives @@ -188,8 +183,14 @@ public static List onlyPrimitiveFieldAttributes(Collection } public static Pipe pipe(Expression e) { + if (e.foldable()) { + return new ConstantInput(e.source(), e, e.fold()); + } if (e instanceof NamedExpression) { - return ((NamedExpression) e).asPipe(); + return new AttributeInput(e.source(), e, ((NamedExpression) e).toAttribute()); + } + if (e instanceof Function) { + return ((Function) e).asPipe(); } throw new SqlIllegalArgumentException("Cannot create pipe for {}", e); } @@ -201,4 +202,8 @@ public static List pipe(List expressions) { } return pipes; } -} + + public static String id(Expression e) { + return Integer.toHexString(e.hashCode()); + } +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/FieldAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/FieldAttribute.java index 625a679898a93..f802c9a940dd1 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/FieldAttribute.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/FieldAttribute.java @@ -36,14 +36,14 @@ public FieldAttribute(Source source, String name, EsField field) { public FieldAttribute(Source source, FieldAttribute parent, String name, EsField field) { this(source, parent, name, field, null, Nullability.TRUE, null, false); } - + public FieldAttribute(Source source, FieldAttribute parent, String name, EsField field, String qualifier, Nullability nullability, - ExpressionId id, boolean synthetic) { + NameId id, boolean synthetic) { this(source, parent, name, field.getDataType(), field, qualifier, nullability, id, synthetic); } public FieldAttribute(Source source, FieldAttribute parent, String name, DataType type, EsField field, String qualifier, - Nullability nullability, ExpressionId id, boolean synthetic) { + Nullability nullability, NameId id, boolean synthetic) { super(source, name, type, qualifier, nullability, id, synthetic); this.path = parent != null ? parent.name() : StringUtils.EMPTY; this.parent = parent; @@ -103,8 +103,8 @@ private FieldAttribute innerField(EsField type) { } @Override - protected Attribute clone(Source source, String name, DataType type, String qualifier, - Nullability nullability, ExpressionId id, boolean synthetic) { + protected Attribute clone(Source source, String name, DataType type, String qualifier, Nullability nullability, NameId id, + boolean synthetic) { FieldAttribute qualifiedParent = parent != null ? (FieldAttribute) parent.withQualifier(qualifier) : null; return new FieldAttribute(source, qualifiedParent, name, field, qualifier, nullability, id, synthetic); } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Literal.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Literal.java index b22483bda3655..315b1bb308eb7 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Literal.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Literal.java @@ -6,23 +6,18 @@ package org.elasticsearch.xpack.sql.expression; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; -import org.elasticsearch.xpack.sql.expression.gen.script.Params; -import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; import org.elasticsearch.xpack.sql.tree.NodeInfo; import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.DataType; import org.elasticsearch.xpack.sql.type.DataTypeConversion; import org.elasticsearch.xpack.sql.type.DataTypes; -import java.util.List; import java.util.Objects; -import static java.util.Collections.emptyList; - /** * SQL Literal or constant. */ -public class Literal extends NamedExpression { +public class Literal extends LeafExpression { public static final Literal TRUE = Literal.of(Source.EMPTY, Boolean.TRUE); public static final Literal FALSE = Literal.of(Source.EMPTY, Boolean.FALSE); @@ -32,11 +27,7 @@ public class Literal extends NamedExpression { private final DataType dataType; public Literal(Source source, Object value, DataType dataType) { - this(source, null, value, dataType); - } - - public Literal(Source source, String name, Object value, DataType dataType) { - super(source, name == null ? source.text() : name, emptyList(), null); + super(source); this.dataType = dataType; this.value = DataTypeConversion.convert(value, dataType); } @@ -75,32 +66,6 @@ public Object fold() { return value; } - @Override - public Attribute toAttribute() { - return new LiteralAttribute(source(), name(), dataType, null, nullable(), id(), false, this); - } - - @Override - public ScriptTemplate asScript() { - return new ScriptTemplate(String.valueOf(value), Params.EMPTY, dataType); - } - - @Override - public Expression replaceChildren(List newChildren) { - throw new UnsupportedOperationException("this type of node doesn't have any children to replace"); - } - - @Override - public AttributeSet references() { - return AttributeSet.EMPTY; - } - - @Override - protected Expression canonicalize() { - String s = String.valueOf(value); - return name().equals(s) ? this : Literal.of(source(), value); - } - @Override public int hashCode() { return Objects.hash(value, dataType); @@ -116,14 +81,17 @@ public boolean equals(Object obj) { } Literal other = (Literal) obj; - return Objects.equals(value, other.value) - && Objects.equals(dataType, other.dataType); + return Objects.equals(value, other.value) && Objects.equals(dataType, other.dataType); } @Override public String toString() { - String s = String.valueOf(value); - return name().equals(s) ? s : name() + "=" + value; + return String.valueOf(value); + } + + @Override + public String nodeString() { + return toString() + "[" + dataType + "]"; } /** @@ -141,31 +109,18 @@ public static Literal of(Source source, Object value) { * Throws an exception if the expression is not foldable. */ public static Literal of(Expression foldable) { - return of((String) null, foldable); - } - - public static Literal of(String name, Expression foldable) { if (!foldable.foldable()) { throw new SqlIllegalArgumentException("Foldable expression required for Literal creation; received unfoldable " + foldable); } if (foldable instanceof Literal) { - Literal l = (Literal) foldable; - if (name == null || l.name().equals(name)) { - return l; - } + return (Literal) foldable; } - Object fold = foldable.fold(); - - if (name == null) { - name = foldable instanceof NamedExpression ? ((NamedExpression) foldable).name() : String.valueOf(fold); - } - return new Literal(foldable.source(), name, fold, foldable.dataType()); + return new Literal(foldable.source(), foldable.fold(), foldable.dataType()); } public static Literal of(Expression source, Object value) { - String name = source instanceof NamedExpression ? ((NamedExpression) source).name() : String.valueOf(value); - return new Literal(source.source(), name, value, source.dataType()); + return new Literal(source.source(), value, source.dataType()); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/LiteralAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/LiteralAttribute.java deleted file mode 100644 index 506f3f8a0732d..0000000000000 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/LiteralAttribute.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.sql.expression; - -import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; -import org.elasticsearch.xpack.sql.tree.NodeInfo; -import org.elasticsearch.xpack.sql.tree.Source; -import org.elasticsearch.xpack.sql.type.DataType; - -public class LiteralAttribute extends TypedAttribute { - - private final Literal literal; - - public LiteralAttribute(Source source, String name, DataType dataType, String qualifier, Nullability nullability, ExpressionId id, - boolean synthetic, Literal literal) { - super(source, name, dataType, qualifier, nullability, id, synthetic); - this.literal = literal; - } - - @Override - protected NodeInfo info() { - return NodeInfo.create(this, LiteralAttribute::new, - name(), dataType(), qualifier(), nullable(), id(), synthetic(), literal); - } - - @Override - protected LiteralAttribute clone(Source source, String name, DataType dataType, String qualifier, Nullability nullability, - ExpressionId id, boolean synthetic) { - return new LiteralAttribute(source, name, dataType, qualifier, nullability, id, synthetic, literal); - } - - @Override - protected String label() { - return "c"; - } - - @Override - public Pipe asPipe() { - return literal.asPipe(); - } - - @Override - public Object fold() { - return literal.fold(); - } -} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ExpressionId.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/NameId.java similarity index 77% rename from x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ExpressionId.java rename to x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/NameId.java index cbc622a615cba..bc74a506d77af 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ExpressionId.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/NameId.java @@ -9,23 +9,23 @@ import java.util.concurrent.atomic.AtomicLong; /** - * Unique identifier for an expression. + * Unique identifier for a named expression. *

    * We use an {@link AtomicLong} to guarantee that they are unique - * and that they produce reproduceable values when run in subsequent - * tests. They don't produce reproduceable values in production, but + * and that create reproducible values when run in subsequent + * tests. They don't produce reproducible values in production, but * you rarely debug with them in production and commonly do so in * tests. */ -public class ExpressionId { +public class NameId { private static final AtomicLong COUNTER = new AtomicLong(); private final long id; - public ExpressionId() { + public NameId() { this.id = COUNTER.incrementAndGet(); } - public ExpressionId(long id) { + public NameId(long id) { this.id = id; } @@ -42,7 +42,7 @@ public boolean equals(Object obj) { if (obj == null || obj.getClass() != getClass()) { return false; } - ExpressionId other = (ExpressionId) obj; + NameId other = (NameId) obj; return id == other.id; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/NamedExpression.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/NamedExpression.java index e586621a7ddb3..633e230393049 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/NamedExpression.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/NamedExpression.java @@ -5,10 +5,6 @@ */ package org.elasticsearch.xpack.sql.expression; -import org.elasticsearch.xpack.sql.expression.gen.pipeline.AttributeInput; -import org.elasticsearch.xpack.sql.expression.gen.pipeline.ConstantInput; -import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; -import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; import org.elasticsearch.xpack.sql.tree.Source; import java.util.List; @@ -21,19 +17,18 @@ public abstract class NamedExpression extends Expression { private final String name; - private final ExpressionId id; + private final NameId id; private final boolean synthetic; - private Pipe lazyPipe = null; - public NamedExpression(Source source, String name, List children, ExpressionId id) { + public NamedExpression(Source source, String name, List children, NameId id) { this(source, name, children, id, false); } - public NamedExpression(Source source, String name, List children, ExpressionId id, boolean synthetic) { + public NamedExpression(Source source, String name, List children, NameId id, boolean synthetic) { super(source, children); this.name = name; - this.id = id == null ? new ExpressionId() : id; + this.id = id == null ? new NameId() : id; this.synthetic = synthetic; } @@ -41,7 +36,7 @@ public String name() { return name; } - public ExpressionId id() { + public NameId id() { return id; } @@ -51,20 +46,6 @@ public boolean synthetic() { public abstract Attribute toAttribute(); - public Pipe asPipe() { - if (lazyPipe == null) { - lazyPipe = foldable() ? new ConstantInput(source(), this, fold()) : makePipe(); - } - - return lazyPipe; - } - - protected Pipe makePipe() { - return new AttributeInput(source(), this, toAttribute()); - } - - public abstract ScriptTemplate asScript(); - @Override public int hashCode() { return Objects.hash(super.hashCode(), name, synthetic); @@ -95,4 +76,9 @@ public boolean equals(Object obj) { public String toString() { return super.toString() + "#" + id(); } -} + + @Override + public String nodeString() { + return name(); + } +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Order.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Order.java index 267a8827d8cd6..3642ac94d8e79 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Order.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Order.java @@ -101,4 +101,4 @@ public boolean equals(Object obj) { && Objects.equals(nulls, other.nulls) && Objects.equals(child, other.child); } -} +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ReferenceAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ReferenceAttribute.java new file mode 100644 index 0000000000000..03330bc1148f5 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ReferenceAttribute.java @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.expression; + +import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.tree.Source; +import org.elasticsearch.xpack.sql.type.DataType; + +/** + * Attribute based on a reference to an expression. + */ +public class ReferenceAttribute extends TypedAttribute { + + public ReferenceAttribute(Source source, String name, DataType dataType) { + this(source, name, dataType, null, Nullability.FALSE, null, false); + } + + public ReferenceAttribute(Source source, String name, DataType dataType, String qualifier, Nullability nullability, + NameId id, boolean synthetic) { + super(source, name, dataType, qualifier, nullability, id, synthetic); + } + + @Override + protected Attribute clone(Source source, String name, DataType dataType, String qualifier, Nullability nullability, NameId id, + boolean synthetic) { + return new ReferenceAttribute(source, name, dataType, qualifier, nullability, id, synthetic); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, ReferenceAttribute::new, name(), dataType(), qualifier(), nullable(), id(), synthetic()); + } + + @Override + protected String label() { + return "r"; + } +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ScalarSubquery.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ScalarSubquery.java index 84693cdc79dfc..cba61814e8f51 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ScalarSubquery.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/ScalarSubquery.java @@ -16,7 +16,7 @@ public ScalarSubquery(Source source, LogicalPlan query) { this(source, query, null); } - public ScalarSubquery(Source source, LogicalPlan query, ExpressionId id) { + public ScalarSubquery(Source source, LogicalPlan query, NameId id) { super(source, query, id); } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/SubQueryExpression.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/SubQueryExpression.java index 17ec60b6e6935..250e5de721846 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/SubQueryExpression.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/SubQueryExpression.java @@ -15,16 +15,16 @@ public abstract class SubQueryExpression extends Expression { private final LogicalPlan query; - private final ExpressionId id; + private final NameId id; public SubQueryExpression(Source source, LogicalPlan query) { this(source, query, null); } - public SubQueryExpression(Source source, LogicalPlan query, ExpressionId id) { + public SubQueryExpression(Source source, LogicalPlan query, NameId id) { super(source, Collections.emptyList()); this.query = query; - this.id = id == null ? new ExpressionId() : id; + this.id = id == null ? new NameId() : id; } @Override @@ -36,7 +36,7 @@ public LogicalPlan query() { return query; } - public ExpressionId id() { + public NameId id() { return id; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/TypedAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/TypedAttribute.java index 414ff330bda8f..98f91d4dca158 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/TypedAttribute.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/TypedAttribute.java @@ -15,7 +15,7 @@ public abstract class TypedAttribute extends Attribute { private final DataType dataType; protected TypedAttribute(Source source, String name, DataType dataType, String qualifier, Nullability nullability, - ExpressionId id, boolean synthetic) { + NameId id, boolean synthetic) { super(source, name, qualifier, nullability, id, synthetic); this.dataType = dataType; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedAlias.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedAlias.java index 178c4d896959f..67bbee18b392e 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedAlias.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedAlias.java @@ -6,14 +6,14 @@ package org.elasticsearch.xpack.sql.expression; import org.elasticsearch.xpack.sql.capabilities.UnresolvedException; -import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.tree.Source; + +import java.util.List; import java.util.Objects; import static java.util.Collections.singletonList; -import java.util.List; - public class UnresolvedAlias extends UnresolvedNamedExpression { private final Expression child; @@ -72,4 +72,9 @@ public boolean equals(Object obj) { public String toString() { return child + " AS ?"; } -} + + @Override + public String nodeString() { + return toString(); + } +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedAttribute.java index add7f702e04d8..34b8eca1c3551 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedAttribute.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedAttribute.java @@ -13,11 +13,8 @@ import org.elasticsearch.xpack.sql.util.CollectionUtils; import java.util.List; -import java.util.Locale; import java.util.Objects; -import static java.lang.String.format; - // unfortunately we can't use UnresolvedNamedExpression public class UnresolvedAttribute extends Attribute implements Unresolvable { @@ -37,7 +34,7 @@ public UnresolvedAttribute(Source source, String name, String qualifier, String this(source, name, qualifier, null, unresolvedMessage, null); } - public UnresolvedAttribute(Source source, String name, String qualifier, ExpressionId id, String unresolvedMessage, + public UnresolvedAttribute(Source source, String name, String qualifier, NameId id, String unresolvedMessage, Object resolutionMetadata) { super(source, name, qualifier, id); this.customMessage = unresolvedMessage != null; @@ -66,7 +63,7 @@ public boolean resolved() { @Override protected Attribute clone(Source source, String name, DataType dataType, String qualifier, Nullability nullability, - ExpressionId id, boolean synthetic) { + NameId id, boolean synthetic) { return this; } @@ -79,11 +76,6 @@ public DataType dataType() { throw new UnresolvedException("dataType", this); } - @Override - public String nodeString() { - return format(Locale.ROOT, "unknown column '%s'", name()); - } - @Override public String toString() { return UNRESOLVED_PREFIX + qualifiedName(); @@ -94,6 +86,11 @@ protected String label() { return UNRESOLVED_PREFIX; } + @Override + public String nodeString() { + return toString(); + } + @Override public String unresolvedMessage() { return unresolvedMsg; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedNamedExpression.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedNamedExpression.java index 4d68d32f37434..5e27180541dfa 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedNamedExpression.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedNamedExpression.java @@ -7,7 +7,6 @@ import org.elasticsearch.xpack.sql.capabilities.Unresolvable; import org.elasticsearch.xpack.sql.capabilities.UnresolvedException; -import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.DataType; @@ -16,7 +15,7 @@ abstract class UnresolvedNamedExpression extends NamedExpression implements Unresolvable { UnresolvedNamedExpression(Source source, List children) { - super(source, "", children, new ExpressionId()); + super(source, "", children, new NameId()); } @Override @@ -30,7 +29,7 @@ public String name() { } @Override - public ExpressionId id() { + public NameId id() { throw new UnresolvedException("id", this); } @@ -43,9 +42,4 @@ public DataType dataType() { public Attribute toAttribute() { throw new UnresolvedException("attribute", this); } - - @Override - public ScriptTemplate asScript() { - throw new UnresolvedException("script", this); - } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedStar.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedStar.java index 4b5a6dfa53758..0f38a12d7963a 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedStar.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/UnresolvedStar.java @@ -6,14 +6,14 @@ package org.elasticsearch.xpack.sql.expression; import org.elasticsearch.xpack.sql.capabilities.UnresolvedException; -import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.tree.Source; + +import java.util.List; import java.util.Objects; import static java.util.Collections.emptyList; -import java.util.List; - public class UnresolvedStar extends UnresolvedNamedExpression { // typically used for nested fields or inner/dotted fields @@ -74,6 +74,11 @@ public String unresolvedMessage() { return "Cannot determine columns for [" + message() + "]"; } + @Override + public String nodeString() { + return toString(); + } + @Override public String toString() { return UNRESOLVED_PREFIX + message(); diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Function.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Function.java index 7724e81525b6e..47e160df57853 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Function.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Function.java @@ -6,42 +6,39 @@ package org.elasticsearch.xpack.sql.expression.function; import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.ExpressionId; import org.elasticsearch.xpack.sql.expression.Expressions; -import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.expression.Nullability; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.ConstantInput; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; import org.elasticsearch.xpack.sql.tree.Source; -import org.elasticsearch.xpack.sql.util.StringUtils; import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.StringJoiner; /** * Any SQL expression with parentheses, like {@code MAX()}, or {@code ABS()}. A * function is always a {@code NamedExpression}. */ -public abstract class Function extends NamedExpression { +public abstract class Function extends Expression { - private final String functionName, name; + private final String functionName = getClass().getSimpleName().toUpperCase(Locale.ROOT); - protected Function(Source source, List children) { - this(source, children, null, false); - } + private Pipe lazyPipe = null; // TODO: Functions supporting distinct should add a dedicated constructor Location, List, boolean - protected Function(Source source, List children, ExpressionId id, boolean synthetic) { - // cannot detect name yet so override the name - super(source, null, children, id, synthetic); - functionName = StringUtils.camelCaseToUnderscore(getClass().getSimpleName()); - name = source.text(); + protected Function(Source source, List children) { + super(source, children); } public final List arguments() { return children(); } - @Override - public String name() { - return name; + public String functionName() { + return functionName; } @Override @@ -49,16 +46,44 @@ public Nullability nullable() { return Expressions.nullable(children()); } - public String functionName() { - return functionName; + @Override + public int hashCode() { + return Objects.hash(getClass(), children()); } - // TODO: ExpressionId might be converted into an Int which could make the String an int as well - public String functionId() { - return id().toString(); + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + Function other = (Function) obj; + return Objects.equals(children(), other.children()); + } + + public Pipe asPipe() { + if (lazyPipe == null) { + lazyPipe = foldable() ? new ConstantInput(source(), this, fold()) : makePipe(); + } + return lazyPipe; } - public boolean functionEquals(Function f) { - return f != null && getClass() == f.getClass() && arguments().equals(f.arguments()); + protected Pipe makePipe() { + throw new UnsupportedOperationException(); } + + @Override + public String nodeString() { + StringJoiner sj = new StringJoiner(",", functionName() + "(", ")"); + for (Expression ex : arguments()) { + sj.add(ex.nodeString()); + } + return sj.toString(); + } + + public abstract ScriptTemplate asScript(); } \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionAttribute.java deleted file mode 100644 index 962fb010c4820..0000000000000 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionAttribute.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.sql.expression.function; - -import org.elasticsearch.xpack.sql.expression.ExpressionId; -import org.elasticsearch.xpack.sql.expression.Nullability; -import org.elasticsearch.xpack.sql.expression.TypedAttribute; -import org.elasticsearch.xpack.sql.tree.Source; -import org.elasticsearch.xpack.sql.type.DataType; - -import java.util.Objects; - -public abstract class FunctionAttribute extends TypedAttribute { - - private final String functionId; - - protected FunctionAttribute(Source source, String name, DataType dataType, String qualifier, Nullability nullability, - ExpressionId id, boolean synthetic, String functionId) { - super(source, name, dataType, qualifier, nullability, id, synthetic); - this.functionId = functionId; - } - - public String functionId() { - return functionId; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode()); - } - - @Override - public boolean equals(Object obj) { - return super.equals(obj); - } -} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Functions.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Functions.java index 46ca0ea91b430..47ca821f4b5b4 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Functions.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Functions.java @@ -8,10 +8,6 @@ import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunction; -import org.elasticsearch.xpack.sql.plan.QueryPlan; - -import java.util.LinkedHashMap; -import java.util.Map; public abstract class Functions { @@ -22,15 +18,4 @@ public static boolean isAggregate(Expression e) { public static boolean isGrouping(Expression e) { return e instanceof GroupingFunction; } - - public static Map collectFunctions(QueryPlan plan) { - Map resolvedFunctions = new LinkedHashMap<>(); - plan.forEachExpressionsDown(e -> { - if (e.resolved() && e instanceof Function) { - Function f = (Function) e; - resolvedFunctions.put(f.functionId(), f); - } - }); - return resolvedFunctions; - } } \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Score.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Score.java index 23363ff6cbddd..d5cee6449807d 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Score.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Score.java @@ -6,12 +6,11 @@ package org.elasticsearch.xpack.sql.expression.function; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; -import org.elasticsearch.xpack.sql.expression.Attribute; import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; -import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.DataType; import java.util.List; @@ -43,11 +42,6 @@ public DataType dataType() { return DataType.FLOAT; } - @Override - public Attribute toAttribute() { - return new ScoreAttribute(source()); - } - @Override public boolean equals(Object obj) { if (obj == null || obj.getClass() != getClass()) { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/ScoreAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/ScoreAttribute.java deleted file mode 100644 index 7d93db3d862f0..0000000000000 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/ScoreAttribute.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.sql.expression.function; - -import org.elasticsearch.xpack.sql.expression.Attribute; -import org.elasticsearch.xpack.sql.expression.ExpressionId; -import org.elasticsearch.xpack.sql.expression.Nullability; -import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; -import org.elasticsearch.xpack.sql.expression.gen.pipeline.ScorePipe; -import org.elasticsearch.xpack.sql.tree.NodeInfo; -import org.elasticsearch.xpack.sql.tree.Source; -import org.elasticsearch.xpack.sql.type.DataType; - -import static org.elasticsearch.xpack.sql.expression.Nullability.FALSE; - -/** - * {@link Attribute} that represents Elasticsearch's {@code _score}. - */ -public class ScoreAttribute extends FunctionAttribute { - /** - * Constructor for normal use. - */ - public ScoreAttribute(Source source) { - this(source, "SCORE()", DataType.FLOAT, null, FALSE, null, false); - } - - /** - * Constructor for {@link #clone()} - */ - private ScoreAttribute(Source source, String name, DataType dataType, String qualifier, Nullability nullability, ExpressionId id, - boolean synthetic) { - super(source, name, dataType, qualifier, nullability, id, synthetic, "SCORE"); - } - - @Override - protected NodeInfo info() { - return NodeInfo.create(this); - } - - @Override - protected Attribute clone(Source source, String name, DataType dataType, String qualifier, Nullability nullability, - ExpressionId id, boolean synthetic) { - return new ScoreAttribute(source, name, dataType, qualifier, nullability, id, synthetic); - } - - @Override - protected Pipe makePipe() { - return new ScorePipe(source(), this); - } - - @Override - protected String label() { - return "SCORE"; - } -} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/UnresolvedFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/UnresolvedFunction.java index 85afc25c5c6cb..920d030ddfd99 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/UnresolvedFunction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/UnresolvedFunction.java @@ -7,7 +7,6 @@ import org.elasticsearch.xpack.sql.capabilities.Unresolvable; import org.elasticsearch.xpack.sql.capabilities.UnresolvedException; -import org.elasticsearch.xpack.sql.expression.Attribute; import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.Nullability; @@ -113,16 +112,10 @@ public boolean resolved() { return false; } - @Override public String name() { return name; } - @Override - public String functionName() { - return name; - } - ResolutionType resolutionType() { return resolutionType; } @@ -149,11 +142,6 @@ public Nullability nullable() { throw new UnresolvedException("nullable", this); } - @Override - public Attribute toAttribute() { - throw new UnresolvedException("attribute", this); - } - @Override public ScriptTemplate asScript() { throw new UnresolvedException("script", this); @@ -169,6 +157,11 @@ public String toString() { return UNRESOLVED_PREFIX + name + children(); } + @Override + public String nodeString() { + return toString(); + } + @Override public boolean equals(Object obj) { if (obj == null || obj.getClass() != getClass()) { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/AggregateFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/AggregateFunction.java index 59b4f345a4a61..91ac02dc83785 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/AggregateFunction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/AggregateFunction.java @@ -30,8 +30,6 @@ public abstract class AggregateFunction extends Function { private final Expression field; private final List parameters; - private AggregateFunctionAttribute lazyAttribute; - protected AggregateFunction(Source source, Expression field) { this(source, field, emptyList()); } @@ -51,18 +49,14 @@ public List parameters() { } @Override - public AggregateFunctionAttribute toAttribute() { - if (lazyAttribute == null) { - // this is highly correlated with QueryFolder$FoldAggregate#addFunction (regarding the function name within the querydsl) - lazyAttribute = new AggregateFunctionAttribute(source(), name(), dataType(), id(), functionId()); - } - return lazyAttribute; + protected TypeResolution resolveType() { + return TypeResolutions.isExact(field, sourceText(), Expressions.ParamOrdinal.DEFAULT); } @Override protected Pipe makePipe() { // unresolved AggNameInput (should always get replaced by the folder) - return new AggNameInput(source(), this, name()); + return new AggNameInput(source(), this, sourceText()); } @Override @@ -71,22 +65,19 @@ public ScriptTemplate asScript() { } @Override - public boolean equals(Object obj) { - if (false == super.equals(obj)) { - return false; - } - AggregateFunction other = (AggregateFunction) obj; - return Objects.equals(other.field(), field()) - && Objects.equals(other.parameters(), parameters()); - } - - @Override - protected TypeResolution resolveType() { - return TypeResolutions.isExact(field, sourceText(), Expressions.ParamOrdinal.DEFAULT); + public int hashCode() { + // NB: the hashcode is currently used for key generation so + // to avoid clashes between aggs with the same arguments, add the class name as variation + return Objects.hash(getClass(), children()); } @Override - public int hashCode() { - return Objects.hash(field(), parameters()); + public boolean equals(Object obj) { + if (super.equals(obj) == true) { + AggregateFunction other = (AggregateFunction) obj; + return Objects.equals(other.field(), field()) + && Objects.equals(other.parameters(), parameters()); + } + return false; } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/AggregateFunctionAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/AggregateFunctionAttribute.java deleted file mode 100644 index 463a277a8fa74..0000000000000 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/AggregateFunctionAttribute.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.sql.expression.function.aggregate; - -import org.elasticsearch.xpack.sql.expression.Attribute; -import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.ExpressionId; -import org.elasticsearch.xpack.sql.expression.Nullability; -import org.elasticsearch.xpack.sql.expression.function.FunctionAttribute; -import org.elasticsearch.xpack.sql.tree.NodeInfo; -import org.elasticsearch.xpack.sql.tree.Source; -import org.elasticsearch.xpack.sql.type.DataType; - -import java.util.Objects; - -public class AggregateFunctionAttribute extends FunctionAttribute { - - // used when dealing with a inner agg (avg -> stats) to keep track of - // packed id - // used since the functionId points to the compoundAgg - private final ExpressionId innerId; - private final String propertyPath; - - AggregateFunctionAttribute(Source source, String name, DataType dataType, ExpressionId id, String functionId) { - this(source, name, dataType, null, Nullability.FALSE, id, false, functionId, null, null); - } - - AggregateFunctionAttribute(Source source, String name, DataType dataType, ExpressionId id, String functionId, ExpressionId innerId, - String propertyPath) { - this(source, name, dataType, null, Nullability.FALSE, id, false, functionId, innerId, propertyPath); - } - - public AggregateFunctionAttribute(Source source, String name, DataType dataType, String qualifier, Nullability nullability, - ExpressionId id, boolean synthetic, String functionId, ExpressionId innerId, String propertyPath) { - super(source, name, dataType, qualifier, nullability, id, synthetic, functionId); - this.innerId = innerId; - this.propertyPath = propertyPath; - } - - @Override - protected NodeInfo info() { - return NodeInfo.create(this, AggregateFunctionAttribute::new, name(), dataType(), qualifier(), nullable(), id(), synthetic(), - functionId(), innerId, propertyPath); - } - - public ExpressionId innerId() { - return innerId != null ? innerId : id(); - } - - public String propertyPath() { - return propertyPath; - } - - @Override - protected Expression canonicalize() { - return new AggregateFunctionAttribute(source(), "", dataType(), null, Nullability.TRUE, id(), false, "", null, null); - } - - @Override - protected Attribute clone(Source source, String name, DataType dataType, String qualifier, Nullability nullability, ExpressionId id, - boolean synthetic) { - // this is highly correlated with QueryFolder$FoldAggregate#addFunction (regarding the function name within the querydsl) - // that is the functionId is actually derived from the expression id to easily track it across contexts - return new AggregateFunctionAttribute(source, name, dataType, qualifier, nullability, id, synthetic, functionId(), innerId, - propertyPath); - } - - public AggregateFunctionAttribute withFunctionId(String functionId, String propertyPath) { - return new AggregateFunctionAttribute(source(), name(), dataType(), qualifier(), nullable(), id(), synthetic(), functionId, innerId, - propertyPath); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), propertyPath); - } - - @Override - public boolean equals(Object obj) { - if (super.equals(obj)) { - AggregateFunctionAttribute other = (AggregateFunctionAttribute) obj; - return Objects.equals(propertyPath, other.propertyPath); - } - return false; - } - - @Override - protected String label() { - return "a->" + innerId(); - } -} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/Count.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/Count.java index 1da2eeb0277ad..951144f5b2eb6 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/Count.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/Count.java @@ -6,8 +6,6 @@ package org.elasticsearch.xpack.sql.expression.function.aggregate; import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.Literal; -import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.tree.NodeInfo; import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.DataType; @@ -52,45 +50,16 @@ public DataType dataType() { } @Override - public String functionId() { - String functionId = id().toString(); - // if count works against a given expression, use its id (to identify the group) - // in case of COUNT DISTINCT don't use the expression id to avoid possible duplicate IDs when COUNT and COUNT DISTINCT is used - // in the same query - if (!distinct() && field() instanceof NamedExpression) { - functionId = ((NamedExpression) field()).id().toString(); - } - return functionId; + public int hashCode() { + return Objects.hash(super.hashCode(), distinct()); } - @Override - public AggregateFunctionAttribute toAttribute() { - // COUNT(*) gets its value from the parent aggregation on which _count is called - if (field() instanceof Literal) { - return new AggregateFunctionAttribute(source(), name(), dataType(), id(), functionId(), id(), "_count"); - } - // COUNT(column) gets its value from a sibling aggregation (an exists filter agg) by calling its id and then _count on it - if (!distinct()) { - return new AggregateFunctionAttribute(source(), name(), dataType(), id(), functionId(), id(), functionId() + "._count"); - } - return super.toAttribute(); - } - @Override public boolean equals(Object obj) { - if (this == obj) { - return true; + if (super.equals(obj) == true) { + Count other = (Count) obj; + return Objects.equals(other.distinct(), distinct()); } - if (obj == null || obj.getClass() != getClass()) { - return false; - } - Count other = (Count) obj; - return Objects.equals(other.distinct(), distinct()) - && Objects.equals(field(), other.field()); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), distinct()); + return false; } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/InnerAggregate.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/InnerAggregate.java index 6e35fa5a7ac72..c9d18b83c156d 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/InnerAggregate.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/aggregate/InnerAggregate.java @@ -6,12 +6,12 @@ package org.elasticsearch.xpack.sql.expression.function.aggregate; import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.function.Function; import org.elasticsearch.xpack.sql.tree.NodeInfo; import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.DataType; import java.util.List; +import java.util.Objects; public class InnerAggregate extends AggregateFunction { @@ -69,38 +69,28 @@ public DataType dataType() { } @Override - public String functionId() { - return outer.id().toString(); + public String functionName() { + return inner.functionName(); } @Override - public AggregateFunctionAttribute toAttribute() { - // this is highly correlated with QueryFolder$FoldAggregate#addFunction (regarding the function name within the querydsl) - return new AggregateFunctionAttribute(source(), name(), dataType(), outer.id(), functionId(), - inner.id(), aggMetricValue(functionId(), innerName)); - } - - private static String aggMetricValue(String aggPath, String valueName) { - // handle aggPath inconsistency (for percentiles and percentileRanks) percentile[99.9] (valid) vs percentile.99.9 (invalid) - return aggPath + "[" + valueName + "]"; + public int hashCode() { + return Objects.hash(inner, outer, innerKey); } @Override - public boolean functionEquals(Function f) { - if (super.equals(f)) { - InnerAggregate other = (InnerAggregate) f; - return inner.equals(other.inner) && outer.equals(other.outer); + public boolean equals(Object obj) { + if (super.equals(obj) == true) { + InnerAggregate other = (InnerAggregate) obj; + return Objects.equals(inner, other.inner) + && Objects.equals(outer, other.outer) + && Objects.equals(innerKey, other.innerKey); } return false; } - @Override - public String name() { - return inner.name(); - } - @Override public String toString() { - return nodeName() + "[" + outer + ">" + inner.nodeName() + "#" + inner.id() + "]"; + return nodeName() + "[" + outer + ">" + inner.nodeName() + "]"; } } \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/grouping/GroupingFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/grouping/GroupingFunction.java index b8a6bb4054095..327c4ef382db0 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/grouping/GroupingFunction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/grouping/GroupingFunction.java @@ -28,8 +28,6 @@ public abstract class GroupingFunction extends Function { private final Expression field; private final List parameters; - private GroupingFunctionAttribute lazyAttribute; - protected GroupingFunction(Source source, Expression field) { this(source, field, emptyList()); } @@ -48,19 +46,10 @@ public List parameters() { return parameters; } - @Override - public GroupingFunctionAttribute toAttribute() { - if (lazyAttribute == null) { - // this is highly correlated with QueryFolder$FoldAggregate#addAggFunction (regarding the function name within the querydsl) - lazyAttribute = new GroupingFunctionAttribute(source(), name(), dataType(), id(), functionId()); - } - return lazyAttribute; - } - @Override protected Pipe makePipe() { // unresolved AggNameInput (should always get replaced by the folder) - return new AggNameInput(source(), this, name()); + return new AggNameInput(source(), this, sourceText()); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/grouping/GroupingFunctionAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/grouping/GroupingFunctionAttribute.java deleted file mode 100644 index 2fed4cf30608b..0000000000000 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/grouping/GroupingFunctionAttribute.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.sql.expression.function.grouping; - -import org.elasticsearch.xpack.sql.expression.Attribute; -import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.ExpressionId; -import org.elasticsearch.xpack.sql.expression.Nullability; -import org.elasticsearch.xpack.sql.expression.function.FunctionAttribute; -import org.elasticsearch.xpack.sql.tree.NodeInfo; -import org.elasticsearch.xpack.sql.tree.Source; -import org.elasticsearch.xpack.sql.type.DataType; - -public class GroupingFunctionAttribute extends FunctionAttribute { - - GroupingFunctionAttribute(Source source, String name, DataType dataType, ExpressionId id, String functionId) { - this(source, name, dataType, null, Nullability.FALSE, id, false, functionId); - } - - public GroupingFunctionAttribute(Source source, String name, DataType dataType, String qualifier, - Nullability nullability, ExpressionId id, boolean synthetic, String functionId) { - super(source, name, dataType, qualifier, nullability, id, synthetic, functionId); - } - - @Override - protected NodeInfo info() { - return NodeInfo.create(this, GroupingFunctionAttribute::new, - name(), dataType(), qualifier(), nullable(), id(), synthetic(), functionId()); - } - - @Override - protected Expression canonicalize() { - return new GroupingFunctionAttribute(source(), "", dataType(), null, Nullability.TRUE, id(), false, ""); - } - - @Override - protected Attribute clone(Source source, String name, DataType dataType, String qualifier, Nullability nullability, - ExpressionId id, boolean synthetic) { - // this is highly correlated with QueryFolder$FoldAggregate#addFunction (regarding the function name within the querydsl) - // that is the functionId is actually derived from the expression id to easily track it across contexts - return new GroupingFunctionAttribute(source, name, dataType, qualifier, nullability, id, synthetic, functionId()); - } - - public GroupingFunctionAttribute withFunctionId(String functionId, String propertyPath) { - return new GroupingFunctionAttribute(source(), name(), dataType(), qualifier(), nullable(), - id(), synthetic(), functionId); - } - - @Override - protected String label() { - return "g->" + functionId(); - } -} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunction.java index d836030a3ae40..b764b4b0a6ac8 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunction.java @@ -22,8 +22,6 @@ */ public abstract class ScalarFunction extends Function implements ScriptWeaver { - private ScalarFunctionAttribute lazyAttribute = null; - protected ScalarFunction(Source source) { super(source, emptyList()); } @@ -32,15 +30,6 @@ protected ScalarFunction(Source source, List fields) { super(source, fields); } - @Override - public final ScalarFunctionAttribute toAttribute() { - if (lazyAttribute == null) { - lazyAttribute = new ScalarFunctionAttribute(source(), name(), dataType(), id(), functionId(), asScript(), orderBy(), - asPipe()); - } - return lazyAttribute; - } - // used if the function is monotonic and thus does not have to be computed for ordering purposes // null means the script needs to be used; expression means the field/expression to be used instead public Expression orderBy() { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunctionAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunctionAttribute.java deleted file mode 100644 index 67324ba466ca5..0000000000000 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunctionAttribute.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.sql.expression.function.scalar; - -import org.elasticsearch.xpack.sql.expression.Attribute; -import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.ExpressionId; -import org.elasticsearch.xpack.sql.expression.Nullability; -import org.elasticsearch.xpack.sql.expression.function.FunctionAttribute; -import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; -import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; -import org.elasticsearch.xpack.sql.tree.NodeInfo; -import org.elasticsearch.xpack.sql.tree.Source; -import org.elasticsearch.xpack.sql.type.DataType; - -import java.util.Objects; - -public class ScalarFunctionAttribute extends FunctionAttribute { - - private final ScriptTemplate script; - private final Expression orderBy; - private final Pipe pipe; - - ScalarFunctionAttribute(Source source, String name, DataType dataType, ExpressionId id, - String functionId, ScriptTemplate script, Expression orderBy, Pipe processorDef) { - this(source, name, dataType, null, Nullability.TRUE, id, false, functionId, script, orderBy, processorDef); - } - - public ScalarFunctionAttribute(Source source, String name, DataType dataType, String qualifier, - Nullability nullability, ExpressionId id, boolean synthetic, String functionId, ScriptTemplate script, - Expression orderBy, Pipe pipe) { - super(source, name, dataType, qualifier, nullability, id, synthetic, functionId); - - this.script = script; - this.orderBy = orderBy; - this.pipe = pipe; - } - - @Override - protected NodeInfo info() { - return NodeInfo.create(this, ScalarFunctionAttribute::new, - name(), dataType(), qualifier(), nullable(), id(), synthetic(), - functionId(), script, orderBy, pipe); - } - - public ScriptTemplate script() { - return script; - } - - public Expression orderBy() { - return orderBy; - } - - @Override - public Pipe asPipe() { - return pipe; - } - - @Override - protected Expression canonicalize() { - return new ScalarFunctionAttribute(source(), "", dataType(), null, Nullability.TRUE, id(), false, - functionId(), script, orderBy, pipe); - } - - @Override - protected Attribute clone(Source source, String name, DataType dataType, String qualifier, Nullability nullability, - ExpressionId id, boolean synthetic) { - return new ScalarFunctionAttribute(source, name, dataType, qualifier, nullability, - id, synthetic, functionId(), script, orderBy, pipe); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), script(), pipe, orderBy); - } - - @Override - public boolean equals(Object obj) { - if (super.equals(obj)) { - ScalarFunctionAttribute other = (ScalarFunctionAttribute) obj; - return Objects.equals(script, other.script()) - && Objects.equals(pipe, other.asPipe()) - && Objects.equals(orderBy, other.orderBy()); - } - return false; - } - - @Override - protected String label() { - return "s->" + functionId(); - } -} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/UnaryScalarFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/UnaryScalarFunction.java index 9a5f85e943124..d10d18b83a1dd 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/UnaryScalarFunction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/UnaryScalarFunction.java @@ -57,8 +57,13 @@ public boolean foldable() { return field.foldable(); } + @Override + public Object fold() { + return makeProcessor().process(field().fold()); + } + @Override public ScriptTemplate asScript() { return asScript(field); } -} +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFunction.java index bda86183fff02..0cce7521a2992 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFunction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFunction.java @@ -52,6 +52,11 @@ public Object fold() { return makeProcessor().process(field().fold()); } + @Override + public int hashCode() { + return Objects.hash(getClass(), field(), zoneId()); + } + @Override public boolean equals(Object obj) { if (obj == null || obj.getClass() != getClass()) { @@ -61,9 +66,4 @@ public boolean equals(Object obj) { return Objects.equals(other.field(), field()) && Objects.equals(other.zoneId(), zoneId()); } - - @Override - public int hashCode() { - return Objects.hash(field(), zoneId()); - } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/StWkttosql.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/StWkttosql.java index 3ebae55dec4f0..04006d4a28b51 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/StWkttosql.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/StWkttosql.java @@ -36,7 +36,7 @@ protected TypeResolution resolveType() { if (field().dataType().isString()) { return TypeResolution.TYPE_RESOLVED; } - return isString(field(), functionName(), Expressions.ParamOrdinal.DEFAULT); + return isString(field(), sourceText(), Expressions.ParamOrdinal.DEFAULT); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/E.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/E.java index 265b96984b581..b1b731fe91be9 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/E.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/E.java @@ -20,7 +20,7 @@ public class E extends MathFunction { private static final ScriptTemplate TEMPLATE = new ScriptTemplate("Math.E", Params.EMPTY, DataType.DOUBLE); public E(Source source) { - super(source, new Literal(source, "E", Math.E, DataType.DOUBLE)); + super(source, new Literal(source, Math.E, DataType.DOUBLE)); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Pi.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Pi.java index 7fb966c3201a3..79492bac3c12e 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Pi.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Pi.java @@ -20,7 +20,7 @@ public class Pi extends MathFunction { private static final ScriptTemplate TEMPLATE = new ScriptTemplate("Math.PI", Params.EMPTY, DataType.DOUBLE); public Pi(Source source) { - super(source, new Literal(source, "PI", Math.PI, DataType.DOUBLE)); + super(source, new Literal(source, Math.PI, DataType.DOUBLE)); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Concat.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Concat.java index 4e461d919a93a..15602bc53c880 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Concat.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Concat.java @@ -38,12 +38,12 @@ protected TypeResolution resolveType() { return new TypeResolution("Unresolved children"); } - TypeResolution resolution = isStringAndExact(left(), functionName(), ParamOrdinal.FIRST); + TypeResolution resolution = isStringAndExact(left(), sourceText(), ParamOrdinal.FIRST); if (resolution.unresolved()) { return resolution; } - return isStringAndExact(right(), functionName(), ParamOrdinal.SECOND); + return isStringAndExact(right(), sourceText(), ParamOrdinal.SECOND); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/Agg.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/Agg.java index 55bba71306250..ad4ff617cce49 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/Agg.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/Agg.java @@ -5,20 +5,48 @@ */ package org.elasticsearch.xpack.sql.expression.gen.script; -import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; +import org.elasticsearch.xpack.sql.expression.Expressions; +import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; +import org.elasticsearch.xpack.sql.expression.function.aggregate.Count; +import org.elasticsearch.xpack.sql.expression.function.aggregate.InnerAggregate; -class Agg extends Param { +class Agg extends Param { - Agg(AggregateFunctionAttribute aggRef) { + private static final String COUNT_PATH = "_count"; + + Agg(AggregateFunction aggRef) { super(aggRef); } String aggName() { - return value().functionId(); + return Expressions.id(value()); } public String aggProperty() { - return value().propertyPath(); + AggregateFunction agg = value(); + + if (agg instanceof InnerAggregate) { + InnerAggregate inner = (InnerAggregate) agg; + return Expressions.id(inner.outer()) + "." + inner.innerName(); + } + // Count needs special handling since in most cases it is not a dedicated aggregation + else if (agg instanceof Count) { + Count c = (Count) agg; + // for literals get the last count + if (c.field().foldable() == true) { + return COUNT_PATH; + } + // when dealing with fields, check whether there's a single-metric (distinct -> cardinality) + // or a bucket (non-distinct - filter agg) + else { + if (c.distinct() == true) { + return Expressions.id(c); + } else { + return Expressions.id(c) + "." + COUNT_PATH; + } + } + } + return null; } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/Grouping.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/Grouping.java index e11f82a842ee0..f34e1c8798f9e 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/Grouping.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/Grouping.java @@ -5,16 +5,16 @@ */ package org.elasticsearch.xpack.sql.expression.gen.script; -import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunctionAttribute; +import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunction; -class Grouping extends Param { +class Grouping extends Param { - Grouping(GroupingFunctionAttribute groupRef) { + Grouping(GroupingFunction groupRef) { super(groupRef); } String groupName() { - return value().functionId(); + return Integer.toHexString(value().hashCode()); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ParamsBuilder.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ParamsBuilder.java index 25e92103cccf5..2e13682b70e7a 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ParamsBuilder.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ParamsBuilder.java @@ -5,8 +5,8 @@ */ package org.elasticsearch.xpack.sql.expression.gen.script; -import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; -import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunctionAttribute; +import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; +import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunction; import java.util.ArrayList; import java.util.List; @@ -24,12 +24,12 @@ public ParamsBuilder variable(Object value) { return this; } - public ParamsBuilder agg(AggregateFunctionAttribute agg) { + public ParamsBuilder agg(AggregateFunction agg) { params.add(new Agg(agg)); return this; } - public ParamsBuilder grouping(GroupingFunctionAttribute grouping) { + public ParamsBuilder grouping(GroupingFunction grouping) { params.add(new Grouping(grouping)); return this; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ScriptWeaver.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ScriptWeaver.java index 223e22b2a33ba..e468a2801ce6c 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ScriptWeaver.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/gen/script/ScriptWeaver.java @@ -7,14 +7,12 @@ package org.elasticsearch.xpack.sql.expression.gen.script; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; -import org.elasticsearch.xpack.sql.expression.Attribute; import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.Expressions; import org.elasticsearch.xpack.sql.expression.FieldAttribute; import org.elasticsearch.xpack.sql.expression.Literal; -import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; -import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunctionAttribute; -import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunctionAttribute; +import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; +import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.sql.expression.function.scalar.geo.GeoShape; import org.elasticsearch.xpack.sql.expression.literal.IntervalDayTime; import org.elasticsearch.xpack.sql.expression.literal.IntervalYearMonth; @@ -36,20 +34,20 @@ default ScriptTemplate asScript(Expression exp) { return scriptWithFoldable(exp); } - Attribute attr = Expressions.attribute(exp); - if (attr != null) { - if (attr instanceof ScalarFunctionAttribute) { - return scriptWithScalar((ScalarFunctionAttribute) attr); - } - if (attr instanceof AggregateFunctionAttribute) { - return scriptWithAggregate((AggregateFunctionAttribute) attr); - } - if (attr instanceof GroupingFunctionAttribute) { - return scriptWithGrouping((GroupingFunctionAttribute) attr); - } - if (attr instanceof FieldAttribute) { - return scriptWithField((FieldAttribute) attr); - } + if (exp instanceof ScalarFunction) { + return scriptWithScalar((ScalarFunction) exp); + } + + if (exp instanceof AggregateFunction) { + return scriptWithAggregate((AggregateFunction) exp); + } + + if (exp instanceof GroupingFunction) { + return scriptWithGrouping((GroupingFunction) exp); + } + + if (exp instanceof FieldAttribute) { + return scriptWithField((FieldAttribute) exp); } throw new SqlIllegalArgumentException("Cannot evaluate script for expression {}", exp); } @@ -108,14 +106,14 @@ default ScriptTemplate scriptWithFoldable(Expression foldable) { dataType()); } - default ScriptTemplate scriptWithScalar(ScalarFunctionAttribute scalar) { - ScriptTemplate nested = scalar.script(); + default ScriptTemplate scriptWithScalar(ScalarFunction scalar) { + ScriptTemplate nested = scalar.asScript(); return new ScriptTemplate(processScript(nested.template()), paramsBuilder().script(nested.params()).build(), dataType()); } - default ScriptTemplate scriptWithAggregate(AggregateFunctionAttribute aggregate) { + default ScriptTemplate scriptWithAggregate(AggregateFunction aggregate) { String template = "{}"; if (aggregate.dataType().isDateBased()) { template = "{sql}.asDateTime({})"; @@ -125,7 +123,7 @@ default ScriptTemplate scriptWithAggregate(AggregateFunctionAttribute aggregate) dataType()); } - default ScriptTemplate scriptWithGrouping(GroupingFunctionAttribute grouping) { + default ScriptTemplate scriptWithGrouping(GroupingFunction grouping) { String template = "{}"; if (grouping.dataType().isDateBased()) { template = "{sql}.asDateTime({})"; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/BinaryPredicate.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/BinaryPredicate.java index eb7265dc29bc8..8705f9c58e578 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/BinaryPredicate.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/BinaryPredicate.java @@ -68,4 +68,9 @@ public String symbol() { public F function() { return function; } + + @Override + public String nodeString() { + return left().nodeString() + " " + symbol() + " " + right().nodeString(); + } } \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/Range.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/Range.java index aa1a3a32e5541..8405b0b436ad4 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/Range.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/Range.java @@ -7,7 +7,6 @@ import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.Expressions; -import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.Nullability; import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; @@ -180,29 +179,4 @@ public boolean equals(Object obj) { && Objects.equals(lower, other.lower) && Objects.equals(upper, other.upper); } - - private static String name(Expression value, Expression lower, Expression upper, boolean includeLower, boolean includeUpper) { - StringBuilder sb = new StringBuilder(); - sb.append(Expressions.name(lower)); - if (!(lower instanceof Literal)) { - sb.insert(0, "("); - sb.append(")"); - } - sb.append(includeLower ? " <= " : " < "); - int pos = sb.length(); - sb.append(Expressions.name(value)); - if (!(value instanceof Literal)) { - sb.insert(pos, "("); - sb.append(")"); - } - sb.append(includeUpper ? " <= " : " < "); - pos = sb.length(); - sb.append(Expressions.name(upper)); - if (!(upper instanceof Literal)) { - sb.insert(pos, "("); - sb.append(")"); - } - - return sb.toString(); - } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/IfConditional.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/IfConditional.java index 298f7d67329b2..96d40f094a168 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/IfConditional.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/IfConditional.java @@ -69,24 +69,17 @@ protected TypeResolution resolveType() { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { - return false; - } - - IfConditional that = (IfConditional) o; - return Objects.equals(condition, that.condition) && - Objects.equals(result, that.result); + public int hashCode() { + return Objects.hash(condition, result); } @Override - public int hashCode() { - return Objects.hash(condition, result); + public boolean equals(Object o) { + if (super.equals(o) == true) { + IfConditional that = (IfConditional) o; + return Objects.equals(condition, that.condition) + && Objects.equals(result, that.result); + } + return false; } -} +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/comparison/In.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/comparison/In.java index 342407c21b32f..4be6d76b8c8f9 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/comparison/In.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/operator/comparison/In.java @@ -119,7 +119,7 @@ protected TypeResolution resolveType() { if (ex.foldable() == false) { return new TypeResolution(format(null, "Comparisons against variables are not (currently) supported; offender [{}] in [{}]", Expressions.name(ex), - name())); + sourceText())); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java index a8145962e15a7..0d7d9acc343d0 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java @@ -11,7 +11,6 @@ import org.elasticsearch.xpack.sql.expression.Attribute; import org.elasticsearch.xpack.sql.expression.AttributeMap; import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.ExpressionId; import org.elasticsearch.xpack.sql.expression.ExpressionSet; import org.elasticsearch.xpack.sql.expression.Expressions; import org.elasticsearch.xpack.sql.expression.FieldAttribute; @@ -19,12 +18,10 @@ import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.expression.Nullability; import org.elasticsearch.xpack.sql.expression.Order; +import org.elasticsearch.xpack.sql.expression.ReferenceAttribute; import org.elasticsearch.xpack.sql.expression.UnresolvedAttribute; import org.elasticsearch.xpack.sql.expression.function.Function; -import org.elasticsearch.xpack.sql.expression.function.FunctionAttribute; -import org.elasticsearch.xpack.sql.expression.function.Functions; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; -import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; import org.elasticsearch.xpack.sql.expression.function.aggregate.ExtendedStats; import org.elasticsearch.xpack.sql.expression.function.aggregate.ExtendedStatsEnclosed; import org.elasticsearch.xpack.sql.expression.function.aggregate.First; @@ -41,8 +38,6 @@ import org.elasticsearch.xpack.sql.expression.function.aggregate.Stats; import org.elasticsearch.xpack.sql.expression.function.aggregate.TopHits; import org.elasticsearch.xpack.sql.expression.function.scalar.Cast; -import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; -import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunctionAttribute; import org.elasticsearch.xpack.sql.expression.predicate.BinaryOperator; import org.elasticsearch.xpack.sql.expression.predicate.BinaryPredicate; import org.elasticsearch.xpack.sql.expression.predicate.Negatable; @@ -52,7 +47,6 @@ import org.elasticsearch.xpack.sql.expression.predicate.conditional.Case; import org.elasticsearch.xpack.sql.expression.predicate.conditional.Coalesce; import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfConditional; -import org.elasticsearch.xpack.sql.expression.predicate.fulltext.FullTextPredicate; import org.elasticsearch.xpack.sql.expression.predicate.logical.And; import org.elasticsearch.xpack.sql.expression.predicate.logical.Not; import org.elasticsearch.xpack.sql.expression.predicate.logical.Or; @@ -127,8 +121,10 @@ protected Iterable.Batch> batches() { Batch pivot = new Batch("Pivot Rewrite", Limiter.ONCE, new RewritePivot()); + Batch refs = new Batch("Replace References", Limiter.ONCE, + new ReplaceReferenceAttributeWithSource()); + Batch operators = new Batch("Operator Optimization", - new PruneDuplicatesInGroupBy(), // combining new CombineProjections(), // folding @@ -145,20 +141,18 @@ protected Iterable.Batch> batches() { new PropagateEquals(), new CombineBinaryComparisons(), // prune/elimination + new PruneLiteralsInGroupBy(), + new PruneDuplicatesInGroupBy(), new PruneFilters(), - new PruneOrderBy(), + new PruneOrderByForImplicitGrouping(), + new PruneLiteralsInOrderBy(), new PruneOrderByNestedFields(), new PruneCast(), // order by alignment of the aggs new SortAggregateOnOrderBy() - // requires changes in the folding - // since the exact same function, with the same ID can appear in multiple places - // see https://github.com/elastic/x-pack-elasticsearch/issues/3527 - //new PruneDuplicateFunctions() ); Batch aggregate = new Batch("Aggregation Rewrite", - //new ReplaceDuplicateAggsWithReferences(), new ReplaceMinMaxWithTopHits(), new ReplaceAggsWithMatrixStats(), new ReplaceAggsWithExtendedStats(), @@ -172,12 +166,11 @@ protected Iterable.Batch> batches() { new SkipQueryOnLimitZero(), new SkipQueryIfFoldingProjection() ); - //new BalanceBooleanTrees()); Batch label = new Batch("Set as Optimized", Limiter.ONCE, CleanAliases.INSTANCE, new SetAsOptimized()); - return Arrays.asList(pivot, operators, aggregate, local, label); + return Arrays.asList(pivot, refs, operators, aggregate, local, label); } static class RewritePivot extends OptimizerRule { @@ -189,17 +182,9 @@ protected LogicalPlan rule(Pivot plan) { for (NamedExpression namedExpression : plan.values()) { // everything should have resolved to an alias if (namedExpression instanceof Alias) { - rawValues.add(((Alias) namedExpression).child()); - } - // TODO: this should be removed when refactoring NamedExpression - else if (namedExpression instanceof Literal) { - rawValues.add(namedExpression); + rawValues.add(Literal.of(((Alias) namedExpression).child())); } - // TODO: NamedExpression refactoring should remove this - else if (namedExpression.foldable()) { - rawValues.add(Literal.of(namedExpression.name(), namedExpression)); - } - // TODO: same as above + // fallback - should not happen else { UnresolvedAttribute attr = new UnresolvedAttribute(namedExpression.source(), namedExpression.name(), null, "Unexpected alias"); @@ -208,589 +193,92 @@ else if (namedExpression.foldable()) { } Filter filter = new Filter(plan.source(), plan.child(), new In(plan.source(), plan.column(), rawValues)); // 2. preserve the PIVOT - return new Pivot(plan.source(), filter, plan.column(), plan.values(), plan.aggregates()); - } - } - - static class PruneDuplicatesInGroupBy extends OptimizerRule { - - @Override - protected LogicalPlan rule(Aggregate agg) { - List groupings = agg.groupings(); - if (groupings.isEmpty()) { - return agg; - } - ExpressionSet unique = new ExpressionSet<>(groupings); - if (unique.size() != groupings.size()) { - return new Aggregate(agg.source(), agg.child(), new ArrayList<>(unique), agg.aggregates()); - } - return agg; - } - } - - static class ReplaceDuplicateAggsWithReferences extends OptimizerRule { - - @Override - protected LogicalPlan rule(Aggregate agg) { - List aggs = agg.aggregates(); - - Map unique = new HashMap<>(); - Map reverse = new HashMap<>(); - - // find duplicates by looking at the function and canonical form - for (NamedExpression ne : aggs) { - if (ne instanceof Alias) { - Alias a = (Alias) ne; - unique.putIfAbsent(a.child(), a); - reverse.putIfAbsent(ne, a.child()); - } - else { - unique.putIfAbsent(ne.canonical(), ne); - reverse.putIfAbsent(ne, ne.canonical()); - } - } - - if (unique.size() != aggs.size()) { - List newAggs = new ArrayList<>(aggs.size()); - for (NamedExpression ne : aggs) { - newAggs.add(unique.get(reverse.get(ne))); - } - return new Aggregate(agg.source(), agg.child(), agg.groupings(), newAggs); - } - - return agg; - } - } - - static class ReplaceAggsWithMatrixStats extends Rule { - - @Override - public LogicalPlan apply(LogicalPlan p) { - Map seen = new LinkedHashMap<>(); - Map promotedFunctionIds = new LinkedHashMap<>(); - - p = p.transformExpressionsUp(e -> rule(e, seen, promotedFunctionIds)); - - // nothing found - if (seen.isEmpty()) { - return p; - } - - return ReplaceAggsWithStats.updateAggAttributes(p, promotedFunctionIds); - } - - @Override - protected LogicalPlan rule(LogicalPlan e) { - return e; - } - - protected Expression rule(Expression e, Map seen, Map promotedIds) { - if (e instanceof MatrixStatsEnclosed) { - AggregateFunction f = (AggregateFunction) e; - - Expression argument = f.field(); - MatrixStats matrixStats = seen.get(argument); - - if (matrixStats == null) { - matrixStats = new MatrixStats(f.source(), argument); - seen.put(argument, matrixStats); - } - - InnerAggregate ia = new InnerAggregate(f.source(), f, matrixStats, argument); - promotedIds.putIfAbsent(f.functionId(), ia.toAttribute()); - return ia; - } - - return e; + return new Pivot(plan.source(), filter, plan.column(), plan.values(), plan.aggregates(), plan.groupings()); } } - static class ReplaceAggsWithExtendedStats extends Rule { - - @Override - public LogicalPlan apply(LogicalPlan p) { - Map promotedFunctionIds = new LinkedHashMap<>(); - Map seen = new LinkedHashMap<>(); - p = p.transformExpressionsUp(e -> rule(e, seen, promotedFunctionIds)); - - // nothing found - if (seen.isEmpty()) { - return p; - } - - // update old agg attributes - return ReplaceAggsWithStats.updateAggAttributes(p, promotedFunctionIds); - } - - @Override - protected LogicalPlan rule(LogicalPlan e) { - return e; - } - - protected Expression rule(Expression e, Map seen, - Map promotedIds) { - if (e instanceof ExtendedStatsEnclosed) { - AggregateFunction f = (AggregateFunction) e; - - Expression argument = f.field(); - ExtendedStats extendedStats = seen.get(argument); - - if (extendedStats == null) { - extendedStats = new ExtendedStats(f.source(), argument); - seen.put(argument, extendedStats); - } - - InnerAggregate ia = new InnerAggregate(f, extendedStats); - promotedIds.putIfAbsent(f.functionId(), ia.toAttribute()); - return ia; - } - - return e; - } - } - - static class ReplaceAggsWithStats extends Rule { - - private static class Match { - final Stats stats; - private final Set> functionTypes = new LinkedHashSet<>(); - private Map, InnerAggregate> innerAggs = null; - - Match(Stats stats) { - this.stats = stats; - } - - @Override - public String toString() { - return stats.toString(); - } - - public void add(Class aggType) { - functionTypes.add(aggType); - } - - // if the stat has at least two different functions for it, promote it as stat - // also keep the promoted function around for reuse - public AggregateFunction maybePromote(AggregateFunction agg) { - if (functionTypes.size() > 1) { - if (innerAggs == null) { - innerAggs = new LinkedHashMap<>(); - } - return innerAggs.computeIfAbsent(agg.getClass(), k -> new InnerAggregate(agg, stats)); - } - return agg; - } - } + // + // Replace any reference attribute with its source, if it does not affect the result. + // This avoid ulterior look-ups between attributes and its source across nodes, which is + // problematic when doing script translation. + // + static class ReplaceReferenceAttributeWithSource extends OptimizerBasicRule { @Override - public LogicalPlan apply(LogicalPlan p) { - Map potentialPromotions = new LinkedHashMap<>(); - - p.forEachExpressionsUp(e -> collect(e, potentialPromotions)); - - // no promotions found - skip - if (potentialPromotions.isEmpty()) { - return p; - } - - // start promotion - - // old functionId to new function attribute - Map promotedFunctionIds = new LinkedHashMap<>(); - - // 1. promote aggs to InnerAggs - p = p.transformExpressionsUp(e -> promote(e, potentialPromotions, promotedFunctionIds)); - - // 2. update the old agg attrs to the promoted agg functions - return updateAggAttributes(p, promotedFunctionIds); - } - - @Override - protected LogicalPlan rule(LogicalPlan e) { - return e; - } - - private Expression collect(Expression e, Map seen) { - if (Stats.isTypeCompatible(e)) { - AggregateFunction f = (AggregateFunction) e; - - Expression argument = f.field(); - Match match = seen.get(argument); - - if (match == null) { - match = new Match(new Stats(new Source(f.sourceLocation(), "STATS(" + Expressions.name(argument) + ")"), argument)); - seen.put(argument, match); - } - match.add(f.getClass()); - } - - return e; - } - - private static Expression promote(Expression e, Map seen, Map attrs) { - if (Stats.isTypeCompatible(e)) { - AggregateFunction f = (AggregateFunction) e; - - Expression argument = f.field(); - Match match = seen.get(argument); - - if (match != null) { - AggregateFunction inner = match.maybePromote(f); - if (inner != f) { - attrs.putIfAbsent(f.functionId(), inner.toAttribute()); - } - return inner; - } - } - return e; - } - - static LogicalPlan updateAggAttributes(LogicalPlan p, Map promotedFunctionIds) { - // 1. update old agg function attributes - p = p.transformExpressionsUp(e -> updateAggFunctionAttrs(e, promotedFunctionIds)); - - // 2. update all scalar function consumers of the promoted aggs - // since they contain the old ids in scrips and processorDefinitions that need regenerating - - // 2a. collect ScalarFunctions that unwrapped refer to any of the updated aggregates - // 2b. replace any of the old ScalarFunction attributes - - final Set newAggIds = new LinkedHashSet<>(promotedFunctionIds.size()); - - for (AggregateFunctionAttribute afa : promotedFunctionIds.values()) { - newAggIds.add(afa.functionId()); - } - - final Map updatedScalarAttrs = new LinkedHashMap<>(); - final Map updatedScalarAliases = new LinkedHashMap<>(); - - p = p.transformExpressionsUp(e -> { - - // replace scalar attributes of the old replaced functions - if (e instanceof ScalarFunctionAttribute) { - ScalarFunctionAttribute sfa = (ScalarFunctionAttribute) e; - // check aliases - sfa = updatedScalarAttrs.getOrDefault(sfa.functionId(), sfa); - // check scalars - sfa = updatedScalarAliases.getOrDefault(sfa.id(), sfa); - return sfa; - } + public LogicalPlan apply(LogicalPlan plan) { + final Map collectRefs = new LinkedHashMap<>(); - // unwrap aliases as they 'hide' functions under their own attributes + // collect aliases + plan.forEachUp(p -> p.forEachExpressionsUp(e -> { if (e instanceof Alias) { - Attribute att = Expressions.attribute(e); - if (att instanceof ScalarFunctionAttribute) { - ScalarFunctionAttribute sfa = (ScalarFunctionAttribute) att; - // the underlying function has been updated - // thus record the alias as well - if (updatedScalarAttrs.containsKey(sfa.functionId())) { - updatedScalarAliases.put(sfa.id(), sfa); - } - } + Alias a = (Alias) e; + collectRefs.put(a.toAttribute(), a.child()); } + })); - else if (e instanceof ScalarFunction && false == Expressions.anyMatch(e.children(), c -> c instanceof FullTextPredicate)) { - ScalarFunction sf = (ScalarFunction) e; - - // if it's a unseen function check if the function children/arguments refers to any of the promoted aggs - if (newAggIds.isEmpty() == false && !updatedScalarAttrs.containsKey(sf.functionId()) && e.anyMatch(c -> { - Attribute a = Expressions.attribute(c); - if (a instanceof FunctionAttribute) { - return newAggIds.contains(((FunctionAttribute) a).functionId()); + plan = plan.transformUp(p -> { + // non attribute defining plans get their references removed + if ((p instanceof Pivot || p instanceof Aggregate || p instanceof Project) == false || p.children().isEmpty()) { + p = p.transformExpressionsOnly(e -> { + if (e instanceof ReferenceAttribute) { + e = collectRefs.getOrDefault(e, e); } - return false; - })) { - // if so, record its attribute - updatedScalarAttrs.put(sf.functionId(), sf.toAttribute()); - } - } - - return e; - }); - - return p; - } - - - private static Expression updateAggFunctionAttrs(Expression e, Map promotedIds) { - if (e instanceof AggregateFunctionAttribute) { - AggregateFunctionAttribute ae = (AggregateFunctionAttribute) e; - AggregateFunctionAttribute promoted = promotedIds.get(ae.functionId()); - if (promoted != null) { - return ae.withFunctionId(promoted.functionId(), promoted.propertyPath()); - } - } - return e; - } - } - - static class PromoteStatsToExtendedStats extends Rule { - - @Override - public LogicalPlan apply(LogicalPlan p) { - Map seen = new LinkedHashMap<>(); - - // count the extended stats - p.forEachExpressionsUp(e -> count(e, seen)); - // then if there's a match, replace the stat inside the InnerAgg - return p.transformExpressionsUp(e -> promote(e, seen)); - } - - @Override - protected LogicalPlan rule(LogicalPlan e) { - return e; - } - - private void count(Expression e, Map seen) { - if (e instanceof InnerAggregate) { - InnerAggregate ia = (InnerAggregate) e; - if (ia.outer() instanceof ExtendedStats) { - ExtendedStats extStats = (ExtendedStats) ia.outer(); - seen.putIfAbsent(extStats.field(), extStats); - } - } - } - - protected Expression promote(Expression e, Map seen) { - if (e instanceof InnerAggregate) { - InnerAggregate ia = (InnerAggregate) e; - if (ia.outer() instanceof Stats) { - Stats stats = (Stats) ia.outer(); - ExtendedStats ext = seen.get(stats.field()); - if (ext != null && stats.field().equals(ext.field())) { - return new InnerAggregate(ia.inner(), ext); - } + return e; + }); } - } - - return e; - } - } - - static class ReplaceAggsWithPercentiles extends Rule { - - @Override - public LogicalPlan apply(LogicalPlan p) { - // percentile per field/expression - Map> percentsPerField = new LinkedHashMap<>(); - - // count gather the percents for each field - p.forEachExpressionsUp(e -> count(e, percentsPerField)); - - Map percentilesPerField = new LinkedHashMap<>(); - // create a Percentile agg for each field (and its associated percents) - percentsPerField.forEach((k, v) -> { - percentilesPerField.put(k, new Percentiles(v.iterator().next().source(), k, new ArrayList<>(v))); + return p; }); - // now replace the agg with pointer to the main ones - Map promotedFunctionIds = new LinkedHashMap<>(); - p = p.transformExpressionsUp(e -> rule(e, percentilesPerField, promotedFunctionIds)); - // finally update all the function references as well - return p.transformExpressionsDown(e -> ReplaceAggsWithStats.updateAggFunctionAttrs(e, promotedFunctionIds)); - } - - private void count(Expression e, Map> percentsPerField) { - if (e instanceof Percentile) { - Percentile p = (Percentile) e; - Expression field = p.field(); - Set percentiles = percentsPerField.get(field); - - if (percentiles == null) { - percentiles = new LinkedHashSet<>(); - percentsPerField.put(field, percentiles); - } - - percentiles.add(p.percent()); - } - } - - protected Expression rule(Expression e, Map percentilesPerField, - Map promotedIds) { - if (e instanceof Percentile) { - Percentile p = (Percentile) e; - Percentiles percentiles = percentilesPerField.get(p.field()); - - InnerAggregate ia = new InnerAggregate(p, percentiles); - promotedIds.putIfAbsent(p.functionId(), ia.toAttribute()); - return ia; - } - - return e; - } - - @Override - protected LogicalPlan rule(LogicalPlan e) { - return e; + return plan; } } - static class ReplaceAggsWithPercentileRanks extends Rule { + static class PruneLiteralsInGroupBy extends OptimizerRule { @Override - public LogicalPlan apply(LogicalPlan p) { - // percentile per field/expression - Map> valuesPerField = new LinkedHashMap<>(); - - // count gather the percents for each field - p.forEachExpressionsUp(e -> count(e, valuesPerField)); - - Map ranksPerField = new LinkedHashMap<>(); - // create a PercentileRanks agg for each field (and its associated values) - valuesPerField.forEach((k, v) -> { - ranksPerField.put(k, new PercentileRanks(v.iterator().next().source(), k, new ArrayList<>(v))); - }); - - // now replace the agg with pointer to the main ones - Map promotedFunctionIds = new LinkedHashMap<>(); - p = p.transformExpressionsUp(e -> rule(e, ranksPerField, promotedFunctionIds)); - // finally update all the function references as well - return p.transformExpressionsDown(e -> ReplaceAggsWithStats.updateAggFunctionAttrs(e, promotedFunctionIds)); - } - - private void count(Expression e, Map> ranksPerField) { - if (e instanceof PercentileRank) { - PercentileRank p = (PercentileRank) e; - Expression field = p.field(); - Set percentiles = ranksPerField.get(field); + protected LogicalPlan rule(Aggregate agg) { + List groupings = agg.groupings(); + List prunedGroupings = new ArrayList<>(); - if (percentiles == null) { - percentiles = new LinkedHashSet<>(); - ranksPerField.put(field, percentiles); + for (Expression g : groupings) { + if (g.foldable()) { + prunedGroupings.add(g); } - - percentiles.add(p.value()); } - } - - protected Expression rule(Expression e, Map ranksPerField, - Map promotedIds) { - if (e instanceof PercentileRank) { - PercentileRank p = (PercentileRank) e; - PercentileRanks ranks = ranksPerField.get(p.field()); - InnerAggregate ia = new InnerAggregate(p, ranks); - promotedIds.putIfAbsent(p.functionId(), ia.toAttribute()); - return ia; + // everything was eliminated, the grouping + if (prunedGroupings.size() > 0) { + List newGroupings = new ArrayList<>(groupings); + newGroupings.removeAll(prunedGroupings); + return new Aggregate(agg.source(), agg.child(), newGroupings, agg.aggregates()); } - return e; - } - - @Override - protected LogicalPlan rule(LogicalPlan e) { - return e; - } - } - - static class ReplaceMinMaxWithTopHits extends OptimizerRule { - - @Override - protected LogicalPlan rule(LogicalPlan plan) { - Map seen = new HashMap<>(); - return plan.transformExpressionsDown(e -> { - if (e instanceof Min) { - Min min = (Min) e; - if (min.field().dataType().isString()) { - TopHits topHits = seen.get(min.id()); - if (topHits != null) { - return topHits; - } - topHits = new First(min.source(), min.field(), null); - seen.put(min.id(), topHits); - return topHits; - } - } - if (e instanceof Max) { - Max max = (Max) e; - if (max.field().dataType().isString()) { - TopHits topHits = seen.get(max.id()); - if (topHits != null) { - return topHits; - } - topHits = new Last(max.source(), max.field(), null); - seen.put(max.id(), topHits); - return topHits; - } - } - return e; - }); + return agg; } } - static class PruneFilters extends OptimizerRule { + static class PruneDuplicatesInGroupBy extends OptimizerRule { @Override - protected LogicalPlan rule(Filter filter) { - Expression condition = filter.condition().transformUp(PruneFilters::foldBinaryLogic); - - if (condition instanceof Literal) { - if (TRUE.equals(condition)) { - return filter.child(); - } - if (FALSE.equals(condition) || Expressions.isNull(condition)) { - return new LocalRelation(filter.source(), new EmptyExecutable(filter.output())); - } - } - - if (!condition.equals(filter.condition())) { - return new Filter(filter.source(), filter.child(), condition); - } - return filter; - } - - private static Expression foldBinaryLogic(Expression expression) { - if (expression instanceof Or) { - Or or = (Or) expression; - boolean nullLeft = Expressions.isNull(or.left()); - boolean nullRight = Expressions.isNull(or.right()); - if (nullLeft && nullRight) { - return Literal.NULL; - } - if (nullLeft) { - return or.right(); - } - if (nullRight) { - return or.left(); - } - } - if (expression instanceof And) { - And and = (And) expression; - if (Expressions.isNull(and.left()) || Expressions.isNull(and.right())) { - return Literal.NULL; - } + protected LogicalPlan rule(Aggregate agg) { + List groupings = agg.groupings(); + if (groupings.isEmpty()) { + return agg; } - return expression; - } - } - - static class ReplaceAliasesInHaving extends OptimizerRule { - - @Override - protected LogicalPlan rule(Filter filter) { - if (filter.child() instanceof Aggregate) { - Expression cond = filter.condition(); - // resolve attributes to their actual - Expression newCondition = cond.transformDown(a -> { - - return a; - }, AggregateFunctionAttribute.class); - - if (newCondition != cond) { - return new Filter(filter.source(), filter.child(), newCondition); - } + ExpressionSet unique = new ExpressionSet<>(groupings); + if (unique.size() != groupings.size()) { + return new Aggregate(agg.source(), agg.child(), new ArrayList<>(unique), agg.aggregates()); } - return filter; + return agg; } } static class PruneOrderByNestedFields extends OptimizerRule { - private void findNested(Expression exp, Map functions, Consumer onFind) { + private void findNested(Expression exp, AttributeMap functions, Consumer onFind) { exp.forEachUp(e -> { - if (e instanceof FunctionAttribute) { - FunctionAttribute sfa = (FunctionAttribute) e; - Function f = functions.get(sfa.functionId()); + if (e instanceof ReferenceAttribute) { + Function f = functions.get(e); if (f != null) { findNested(f, functions, onFind); } @@ -810,8 +298,22 @@ protected LogicalPlan rule(Project project) { if (project.child() instanceof OrderBy) { OrderBy ob = (OrderBy) project.child(); - // resolve function aliases (that are hiding the target) - Map functions = Functions.collectFunctions(project); + // resolve function references (that maybe hiding the target) + final Map collectRefs = new LinkedHashMap<>(); + + // collect Attribute sources + // only Aliases are interesting since these are the only ones that hide expressions + // FieldAttribute for example are self replicating. + project.forEachUp(p -> p.forEachExpressionsUp(e -> { + if (e instanceof Alias) { + Alias a = (Alias) e; + if (a.child() instanceof Function) { + collectRefs.put(a.toAttribute(), (Function) a.child()); + } + } + })); + + AttributeMap functions = new AttributeMap<>(collectRefs); // track the direct parents Map nestedOrders = new LinkedHashMap<>(); @@ -870,7 +372,33 @@ protected LogicalPlan rule(Project project) { } } - static class PruneOrderBy extends OptimizerRule { + static class PruneLiteralsInOrderBy extends OptimizerRule { + + @Override + protected LogicalPlan rule(OrderBy ob) { + List prunedOrders = new ArrayList<>(); + + for (Order o : ob.order()) { + if (o.child().foldable()) { + prunedOrders.add(o); + } + } + + // everything was eliminated, the order isn't needed anymore + if (prunedOrders.size() == ob.order().size()) { + return ob.child(); + } + if (prunedOrders.size() > 0) { + List newOrders = new ArrayList<>(ob.order()); + newOrders.removeAll(prunedOrders); + return new OrderBy(ob.source(), ob.child(), newOrders); + } + + return ob; + } + } + + static class PruneOrderByForImplicitGrouping extends OptimizerRule { @Override protected LogicalPlan rule(OrderBy ob) { @@ -905,12 +433,10 @@ static class SortAggregateOnOrderBy extends OptimizerRule { protected LogicalPlan rule(OrderBy ob) { List order = ob.order(); - // remove constants and put the items in reverse order so the iteration happens back to front + // put the items in reverse order so the iteration happens back to front List nonConstant = new LinkedList<>(); - for (Order o : order) { - if (o.child().foldable() == false) { - nonConstant.add(0, o); - } + for (int i = order.size() - 1; i >= 0; i--) { + nonConstant.add(order.get(i)); } Holder foundAggregate = new Holder<>(Boolean.FALSE); @@ -940,13 +466,13 @@ protected LogicalPlan rule(OrderBy ob) { if ((equalsAsAttribute(child, group) && (equalsAsAttribute(alias, fieldToOrder) || equalsAsAttribute(child, fieldToOrder))) || (equalsAsAttribute(alias, group) - && (equalsAsAttribute(alias, fieldToOrder) || equalsAsAttribute(child, fieldToOrder)))) { + && (equalsAsAttribute(alias, fieldToOrder) || equalsAsAttribute(child, fieldToOrder)))) { isMatching.set(Boolean.TRUE); } } }); } - + if (isMatching.get() == true) { // move grouping in front groupings.remove(group); @@ -985,75 +511,18 @@ public LogicalPlan apply(LogicalPlan plan) { @Override protected LogicalPlan rule(LogicalPlan plan) { - final Map replacedCast = new LinkedHashMap<>(); - // eliminate redundant casts LogicalPlan transformed = plan.transformExpressionsUp(e -> { - if (e instanceof Cast) { - Cast c = (Cast) e; - - if (c.from() == c.to()) { - Expression argument = c.field(); - Alias as = new Alias(c.source(), c.sourceText(), argument); - replacedCast.put(c.toAttribute(), as.toAttribute()); - - return as; - } - } - return e; - }); - - // replace attributes from previous removed Casts - if (!replacedCast.isEmpty()) { - return transformed.transformUp(p -> { - List newProjections = new ArrayList<>(); - - boolean changed = false; - for (NamedExpression ne : p.projections()) { - Attribute found = replacedCast.get(ne.toAttribute()); - if (found != null) { - changed = true; - newProjections.add(found); - } - else { - newProjections.add(ne.toAttribute()); - } - } - - return changed ? new Project(p.source(), p.child(), newProjections) : p; - - }, Project.class); - } - return transformed; - } - } - - static class PruneDuplicateFunctions extends Rule { - - @Override - public LogicalPlan apply(LogicalPlan p) { - List seen = new ArrayList<>(); - return p.transformExpressionsUp(e -> rule(e, seen)); - } - - @Override - protected LogicalPlan rule(LogicalPlan e) { - return e; - } - - protected Expression rule(Expression exp, List seen) { - Expression e = exp; - if (e instanceof Function) { - Function f = (Function) e; - for (Function seenFunction : seen) { - if (seenFunction != f && f.functionEquals(seenFunction)) { - return seenFunction; + if (e instanceof Cast) { + Cast c = (Cast) e; + if (c.from() == c.to()) { + return c.field(); } } - seen.add(f); - } + return e; + }); - return exp; + return transformed; } } @@ -1203,12 +672,12 @@ static class FoldNull extends OptimizerExpressionRule { protected Expression rule(Expression e) { if (e instanceof IsNotNull) { if (((IsNotNull) e).field().nullable() == Nullability.FALSE) { - return new Literal(e.source(), Expressions.name(e), Boolean.TRUE, DataType.BOOLEAN); + return new Literal(e.source(), Boolean.TRUE, DataType.BOOLEAN); } } else if (e instanceof IsNull) { if (((IsNull) e).field().nullable() == Nullability.FALSE) { - return new Literal(e.source(), Expressions.name(e), Boolean.FALSE, DataType.BOOLEAN); + return new Literal(e.source(), Boolean.FALSE, DataType.BOOLEAN); } } else if (e instanceof In) { @@ -1220,8 +689,8 @@ protected Expression rule(Expression e) { } else if (e instanceof Alias == false && e.nullable() == Nullability.TRUE && Expressions.anyMatch(e.children(), Expressions::isNull)) { - return Literal.of(e, null); - } + return Literal.of(e, null); + } return e; } @@ -1235,7 +704,7 @@ static class ConstantFolding extends OptimizerExpressionRule { @Override protected Expression rule(Expression e) { - return e.foldable() ? Literal.of(e) : e; + return e.foldable() && (e instanceof Literal == false) ? Literal.of(e) : e; } } @@ -1408,6 +877,7 @@ private Expression simplifyAndOr(BinaryPredicate bc) { return bc; } + @SuppressWarnings("rawtypes") private Expression simplifyNot(Not n) { Expression c = n.field(); @@ -1666,14 +1136,14 @@ private Expression combine(And and) { // />= else if ((other instanceof GreaterThan || other instanceof GreaterThanOrEqual) && (main instanceof LessThan || main instanceof LessThanOrEqual)) { - bcs.remove(j); - bcs.remove(i); + bcs.remove(j); + bcs.remove(i); ranges.add(new Range(and.source(), main.left(), other.right(), other instanceof GreaterThanOrEqual, main.right(), main instanceof LessThanOrEqual)); - changed = true; + changed = true; } } } @@ -1745,16 +1215,16 @@ private static boolean findExistingRange(Range main, List ranges, boolean lowerEq = comp == 0 && main.includeLower() == other.includeLower(); // AND if (conjunctive) { - // (2 < a < 3) AND (1 < a < 3) -> (1 < a < 3) + // (2 < a < 3) AND (1 < a < 3) -> (1 < a < 3) lower = comp > 0 || - // (2 < a < 3) AND (2 < a <= 3) -> (2 < a < 3) + // (2 < a < 3) AND (2 < a <= 3) -> (2 < a < 3) (comp == 0 && !main.includeLower() && other.includeLower()); } // OR else { - // (1 < a < 3) OR (2 < a < 3) -> (1 < a < 3) + // (1 < a < 3) OR (2 < a < 3) -> (1 < a < 3) lower = comp < 0 || - // (2 <= a < 3) OR (2 < a < 3) -> (2 <= a < 3) + // (2 <= a < 3) OR (2 < a < 3) -> (2 <= a < 3) (comp == 0 && main.includeLower() && !other.includeLower()) || lowerEq; } } @@ -1771,16 +1241,16 @@ private static boolean findExistingRange(Range main, List ranges, boolean // AND if (conjunctive) { - // (1 < a < 2) AND (1 < a < 3) -> (1 < a < 2) + // (1 < a < 2) AND (1 < a < 3) -> (1 < a < 2) upper = comp < 0 || - // (1 < a < 2) AND (1 < a <= 2) -> (1 < a < 2) + // (1 < a < 2) AND (1 < a <= 2) -> (1 < a < 2) (comp == 0 && !main.includeUpper() && other.includeUpper()); } // OR else { - // (1 < a < 3) OR (1 < a < 2) -> (1 < a < 3) + // (1 < a < 3) OR (1 < a < 2) -> (1 < a < 3) upper = comp > 0 || - // (1 < a <= 3) OR (1 < a < 3) -> (2 < a < 3) + // (1 < a <= 3) OR (1 < a < 3) -> (2 < a < 3) (comp == 0 && main.includeUpper() && !other.includeUpper()) || upperEq; } } @@ -1839,7 +1309,7 @@ private boolean findConjunctiveComparisonInRange(BinaryComparison main, List 2 < a < 3 boolean lowerEq = comp == 0 && other.includeLower() && main instanceof GreaterThan; - // 2 < a AND (1 < a < 3) -> 2 < a < 3 + // 2 < a AND (1 < a < 3) -> 2 < a < 3 boolean lower = comp > 0 || lowerEq; if (lower) { @@ -1904,18 +1374,18 @@ private static boolean findExistingComparison(BinaryComparison main, List 3 AND a > 2 -> a > 3 - (compare > 0 || - // a > 2 AND a >= 2 -> a > 2 - (compare == 0 && main instanceof GreaterThan && other instanceof GreaterThanOrEqual))) - || - // OR - (!conjunctive && - // a > 2 OR a > 3 -> a > 2 - (compare < 0 || - // a >= 2 OR a > 2 -> a >= 2 + // a > 3 AND a > 2 -> a > 3 + (compare > 0 || + // a > 2 AND a >= 2 -> a > 2 + (compare == 0 && main instanceof GreaterThan && other instanceof GreaterThanOrEqual))) + || + // OR + (!conjunctive && + // a > 2 OR a > 3 -> a > 2 + (compare < 0 || + // a >= 2 OR a > 2 -> a >= 2 (compare == 0 && main instanceof GreaterThanOrEqual && other instanceof GreaterThan)))) { bcs.remove(i); bcs.add(i, main); @@ -1931,40 +1401,365 @@ private static boolean findExistingComparison(BinaryComparison main, List a < 2 - (compare < 0 || - // a < 2 AND a <= 2 -> a < 2 + if (compare != null) { + // AND + if ((conjunctive && + // a < 2 AND a < 3 -> a < 2 + (compare < 0 || + // a < 2 AND a <= 2 -> a < 2 (compare == 0 && main instanceof LessThan && other instanceof LessThanOrEqual))) || - // OR - (!conjunctive && - // a < 2 OR a < 3 -> a < 3 - (compare > 0 || - // a <= 2 OR a < 2 -> a <= 2 - (compare == 0 && main instanceof LessThanOrEqual && other instanceof LessThan)))) { - bcs.remove(i); - bcs.add(i, main); + // OR + (!conjunctive && + // a < 2 OR a < 3 -> a < 3 + (compare > 0 || + // a <= 2 OR a < 2 -> a <= 2 + (compare == 0 && main instanceof LessThanOrEqual && other instanceof LessThan)))) { + bcs.remove(i); + bcs.add(i, main); + } + // found a match + return true; + } + + return false; } - // found a match - return true; } + } - return false; + return false; + } + } + + + static class ReplaceAggsWithMatrixStats extends OptimizerBasicRule { + + @Override + public LogicalPlan apply(LogicalPlan p) { + // minimal reuse of the same matrix stat object + final Map seen = new LinkedHashMap<>(); + + return p.transformExpressionsUp(e -> { + if (e instanceof MatrixStatsEnclosed) { + AggregateFunction f = (AggregateFunction) e; + + Expression argument = f.field(); + MatrixStats matrixStats = seen.get(argument); + + if (matrixStats == null) { + Source source = new Source(f.sourceLocation(), "MATRIX(" + argument.sourceText() + ")"); + matrixStats = new MatrixStats(source, argument); + seen.put(argument, matrixStats); + } + + InnerAggregate ia = new InnerAggregate(f.source(), f, matrixStats, argument); + return ia; + } + + return e; + }); + } + } + + static class ReplaceAggsWithExtendedStats extends OptimizerBasicRule { + + @Override + public LogicalPlan apply(LogicalPlan p) { + // minimal reuse of the same matrix stat object + final Map seen = new LinkedHashMap<>(); + + return p.transformExpressionsUp(e -> { + if (e instanceof ExtendedStatsEnclosed) { + AggregateFunction f = (AggregateFunction) e; + + Expression argument = f.field(); + ExtendedStats extendedStats = seen.get(argument); + + if (extendedStats == null) { + Source source = new Source(f.sourceLocation(), "EXT_STATS(" + argument.sourceText() + ")"); + extendedStats = new ExtendedStats(source, argument); + seen.put(argument, extendedStats); + } + + InnerAggregate ia = new InnerAggregate(f, extendedStats); + return ia; + } + + return e; + }); + } + } + + static class ReplaceAggsWithStats extends OptimizerBasicRule { + + private static class Match { + final Stats stats; + private final Set> functionTypes = new LinkedHashSet<>(); + private Map, InnerAggregate> innerAggs = null; + + Match(Stats stats) { + this.stats = stats; + } + + @Override + public String toString() { + return stats.toString(); + } + + public void add(Class aggType) { + functionTypes.add(aggType); + } + + // if the stat has at least two different functions for it, promote it as stat + // also keep the promoted function around for reuse + public AggregateFunction maybePromote(AggregateFunction agg) { + if (functionTypes.size() > 1) { + if (innerAggs == null) { + innerAggs = new LinkedHashMap<>(); } + return innerAggs.computeIfAbsent(agg.getClass(), k -> new InnerAggregate(agg, stats)); } + return agg; } + } - return false; + @Override + public LogicalPlan apply(LogicalPlan p) { + // 1. first check whether there are at least 2 aggs for the same fields so that there can be a promotion + final Map potentialPromotions = new LinkedHashMap<>(); + + p.forEachExpressionsUp(e -> { + if (Stats.isTypeCompatible(e)) { + AggregateFunction f = (AggregateFunction) e; + + Expression argument = f.field(); + Match match = potentialPromotions.get(argument); + + if (match == null) { + Source source = new Source(f.sourceLocation(), "STATS(" + argument.sourceText() + ")"); + match = new Match(new Stats(source, argument)); + potentialPromotions.put(argument, match); + } + match.add(f.getClass()); + } + }); + + // no promotions found - skip + if (potentialPromotions.isEmpty()) { + return p; + } + + // start promotion + + // 2. promote aggs to InnerAggs + return p.transformExpressionsUp(e -> { + if (Stats.isTypeCompatible(e)) { + AggregateFunction f = (AggregateFunction) e; + + Expression argument = f.field(); + Match match = potentialPromotions.get(argument); + + if (match != null) { + return match.maybePromote(f); + } + } + return e; + }); + } + } + + static class PromoteStatsToExtendedStats extends OptimizerBasicRule { + + @Override + public LogicalPlan apply(LogicalPlan p) { + final Map seen = new LinkedHashMap<>(); + + // count the extended stats + p.forEachExpressionsUp(e -> { + if (e instanceof InnerAggregate) { + InnerAggregate ia = (InnerAggregate) e; + if (ia.outer() instanceof ExtendedStats) { + ExtendedStats extStats = (ExtendedStats) ia.outer(); + seen.putIfAbsent(extStats.field(), extStats); + } + } + }); + + // then if there's a match, replace the stat inside the InnerAgg + return p.transformExpressionsUp(e -> { + if (e instanceof InnerAggregate) { + InnerAggregate ia = (InnerAggregate) e; + if (ia.outer() instanceof Stats) { + Stats stats = (Stats) ia.outer(); + ExtendedStats ext = seen.get(stats.field()); + if (ext != null && stats.field().equals(ext.field())) { + return new InnerAggregate(ia.inner(), ext); + } + } + } + + return e; + }); + } + } + + static class ReplaceAggsWithPercentiles extends OptimizerBasicRule { + + @Override + public LogicalPlan apply(LogicalPlan p) { + // percentile per field/expression + Map> percentsPerField = new LinkedHashMap<>(); + + // count gather the percents for each field + p.forEachExpressionsUp(e -> { + if (e instanceof Percentile) { + Percentile per = (Percentile) e; + Expression field = per.field(); + Set percentiles = percentsPerField.get(field); + + if (percentiles == null) { + percentiles = new LinkedHashSet<>(); + percentsPerField.put(field, percentiles); + } + + percentiles.add(per.percent()); + } + }); + + Map percentilesPerField = new LinkedHashMap<>(); + // create a Percentile agg for each field (and its associated percents) + percentsPerField.forEach((k, v) -> { + percentilesPerField.put(k, new Percentiles(v.iterator().next().source(), k, new ArrayList<>(v))); + }); + + return p.transformExpressionsUp(e -> { + if (e instanceof Percentile) { + Percentile per = (Percentile) e; + Percentiles percentiles = percentilesPerField.get(per.field()); + return new InnerAggregate(per, percentiles); + } + + return e; + }); + } + } + + static class ReplaceAggsWithPercentileRanks extends OptimizerBasicRule { + + @Override + public LogicalPlan apply(LogicalPlan p) { + // percentile per field/expression + final Map> percentPerField = new LinkedHashMap<>(); + + // count gather the percents for each field + p.forEachExpressionsUp(e -> { + if (e instanceof PercentileRank) { + PercentileRank per = (PercentileRank) e; + Expression field = per.field(); + Set percentiles = percentPerField.get(field); + + if (percentiles == null) { + percentiles = new LinkedHashSet<>(); + percentPerField.put(field, percentiles); + } + + percentiles.add(per.value()); + } + }); + + Map ranksPerField = new LinkedHashMap<>(); + // create a PercentileRanks agg for each field (and its associated values) + percentPerField.forEach((k, v) -> { + ranksPerField.put(k, new PercentileRanks(v.iterator().next().source(), k, new ArrayList<>(v))); + }); + + return p.transformExpressionsUp(e -> { + if (e instanceof PercentileRank) { + PercentileRank per = (PercentileRank) e; + PercentileRanks ranks = ranksPerField.get(per.field()); + return new InnerAggregate(per, ranks); + } + + return e; + }); + } + } + + static class ReplaceMinMaxWithTopHits extends OptimizerRule { + + @Override + protected LogicalPlan rule(LogicalPlan plan) { + Map mins = new HashMap<>(); + Map maxs = new HashMap<>(); + return plan.transformExpressionsDown(e -> { + if (e instanceof Min) { + Min min = (Min) e; + if (min.field().dataType().isString()) { + return mins.computeIfAbsent(min.field(), k -> new First(min.source(), k, null)); + } + } + if (e instanceof Max) { + Max max = (Max) e; + if (max.field().dataType().isString()) { + return maxs.computeIfAbsent(max.field(), k -> new Last(max.source(), k, null)); + } + } + return e; + }); + } + } + + static class PruneFilters extends OptimizerRule { + + @Override + protected LogicalPlan rule(Filter filter) { + Expression condition = filter.condition().transformUp(PruneFilters::foldBinaryLogic); + + if (condition instanceof Literal) { + if (TRUE.equals(condition)) { + return filter.child(); + } + if (FALSE.equals(condition) || Expressions.isNull(condition)) { + return new LocalRelation(filter.source(), new EmptyExecutable(filter.output())); + } + } + + if (!condition.equals(filter.condition())) { + return new Filter(filter.source(), filter.child(), condition); + } + return filter; + } + + private static Expression foldBinaryLogic(Expression expression) { + if (expression instanceof Or) { + Or or = (Or) expression; + boolean nullLeft = Expressions.isNull(or.left()); + boolean nullRight = Expressions.isNull(or.right()); + if (nullLeft && nullRight) { + return Literal.NULL; + } + if (nullLeft) { + return or.right(); + } + if (nullRight) { + return or.left(); + } + } + if (expression instanceof And) { + And and = (And) expression; + if (Expressions.isNull(and.left()) || Expressions.isNull(and.right())) { + return Literal.NULL; + } + } + return expression; } } + static class SkipQueryOnLimitZero extends OptimizerRule { @Override protected LogicalPlan rule(Limit limit) { @@ -2104,6 +1899,17 @@ protected LogicalPlan rule(LogicalPlan plan) { protected abstract Expression rule(Expression e); } + abstract static class OptimizerBasicRule extends Rule { + + @Override + public abstract LogicalPlan apply(LogicalPlan plan); + + @Override + protected LogicalPlan rule(LogicalPlan plan) { + return plan; + } + } + enum TransformDirection { UP, DOWN } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/ExpressionBuilder.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/ExpressionBuilder.java index 5a1e09f602a48..524d4e8b75a8e 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/ExpressionBuilder.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/ExpressionBuilder.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.sql.expression.Order; import org.elasticsearch.xpack.sql.expression.Order.NullsPosition; import org.elasticsearch.xpack.sql.expression.ScalarSubquery; +import org.elasticsearch.xpack.sql.expression.UnresolvedAlias; import org.elasticsearch.xpack.sql.expression.UnresolvedAttribute; import org.elasticsearch.xpack.sql.expression.UnresolvedStar; import org.elasticsearch.xpack.sql.expression.function.Function; @@ -157,10 +158,8 @@ public Expression visitSingleExpression(SingleExpressionContext ctx) { public Expression visitSelectExpression(SelectExpressionContext ctx) { Expression exp = expression(ctx.expression()); String alias = visitIdentifier(ctx.identifier()); - if (alias != null) { - exp = new Alias(source(ctx), alias, exp); - } - return exp; + Source source = source(ctx); + return alias != null ? new Alias(source, alias, exp) : new UnresolvedAlias(source, exp); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/Pivot.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/Pivot.java index fe06e1bb01869..35447ecb40510 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/Pivot.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/Pivot.java @@ -6,21 +6,25 @@ package org.elasticsearch.xpack.sql.plan.logical; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; import org.elasticsearch.xpack.sql.capabilities.Resolvables; +import org.elasticsearch.xpack.sql.expression.Alias; import org.elasticsearch.xpack.sql.expression.Attribute; +import org.elasticsearch.xpack.sql.expression.AttributeMap; import org.elasticsearch.xpack.sql.expression.AttributeSet; import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.ExpressionId; import org.elasticsearch.xpack.sql.expression.Expressions; +import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.expression.function.Function; import org.elasticsearch.xpack.sql.tree.NodeInfo; import org.elasticsearch.xpack.sql.tree.Source; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; import static java.util.Collections.singletonList; @@ -29,49 +33,48 @@ public class Pivot extends UnaryPlan { private final Expression column; private final List values; private final List aggregates; + private final List grouping; // derived properties private AttributeSet groupingSet; private AttributeSet valueOutput; private List output; + private AttributeMap aliases; public Pivot(Source source, LogicalPlan child, Expression column, List values, List aggregates) { + this(source, child, column, values, aggregates, null); + } + + public Pivot(Source source, LogicalPlan child, Expression column, List values, List aggregates, + List grouping) { super(source, child); this.column = column; this.values = values; this.aggregates = aggregates; - } - - private static Expression withQualifierNull(Expression e) { - if (e instanceof Attribute) { - Attribute fa = (Attribute) e; - return fa.withQualifier(null); + + // resolve the grouping set ASAP so it doesn't get re-resolved after analysis (since the aliasing information has been removed) + if (grouping == null && expressionsResolved()) { + AttributeSet columnSet = Expressions.references(singletonList(column)); + // grouping can happen only on "primitive" fields, thus exclude multi-fields or nested docs + // the verifier enforces this rule so it does not catch folks by surprise + grouping = new ArrayList<>(new AttributeSet(Expressions.onlyPrimitiveFieldAttributes(child().output())) + // make sure to have the column as the last entry (helps with translation) so substract it + .subtract(columnSet) + .subtract(Expressions.references(aggregates)) + .combine(columnSet)); } - return e; + + this.grouping = grouping; + this.groupingSet = grouping != null ? new AttributeSet(grouping) : null; } @Override protected NodeInfo info() { - return NodeInfo.create(this, Pivot::new, child(), column, values, aggregates); + return NodeInfo.create(this, Pivot::new, child(), column, values, aggregates, grouping); } @Override protected Pivot replaceChild(LogicalPlan newChild) { - Expression newColumn = column; - List newAggregates = aggregates; - - if (newChild instanceof EsRelation) { - // when changing from a SubQueryAlias to EsRelation - // the qualifier of the column and aggregates needs - // to be changed to null like the attributes of EsRelation - // otherwise they don't equal and aren't removed - // when calculating the groupingSet - newColumn = column.transformUp(Pivot::withQualifierNull); - newAggregates = aggregates.stream().map((NamedExpression aggregate) -> - (NamedExpression) aggregate.transformUp(Pivot::withQualifierNull) - ).collect(Collectors.toUnmodifiableList()); - } - - return new Pivot(source(), newChild, newColumn, values, newAggregates); + return new Pivot(source(), newChild, column, values, aggregates, grouping); } public Expression column() { @@ -86,38 +89,39 @@ public List aggregates() { return aggregates; } + public List groupings() { + return grouping; + } + public AttributeSet groupingSet() { if (groupingSet == null) { - AttributeSet columnSet = Expressions.references(singletonList(column)); - // grouping can happen only on "primitive" fields, thus exclude multi-fields or nested docs - // the verifier enforces this rule so it does not catch folks by surprise - groupingSet = new AttributeSet(Expressions.onlyPrimitiveFieldAttributes(child().output())) - // make sure to have the column as the last entry (helps with translation) - .subtract(columnSet) - .subtract(Expressions.references(aggregates)) - .combine(columnSet); + throw new SqlIllegalArgumentException("Cannot determine grouping in unresolved PIVOT"); } return groupingSet; } - public AttributeSet valuesOutput() { - // TODO: the generated id is a hack since it can clash with other potentially generated ids + private AttributeSet valuesOutput() { if (valueOutput == null) { List out = new ArrayList<>(aggregates.size() * values.size()); if (aggregates.size() == 1) { NamedExpression agg = aggregates.get(0); for (NamedExpression value : values) { - ExpressionId id = value.id(); - out.add(value.toAttribute().withDataType(agg.dataType()).withId(id)); + out.add(value.toAttribute().withDataType(agg.dataType())); } } // for multiple args, concat the function and the value else { for (NamedExpression agg : aggregates) { - String name = agg instanceof Function ? ((Function) agg).functionName() : agg.name(); + String name = agg.name(); + if (agg instanceof Alias) { + Alias a = (Alias) agg; + if (a.child() instanceof Function) { + name = ((Function) a.child()).functionName(); + } + } + //FIXME: the value attributes are reused and thus will clash - new ids need to be created for (NamedExpression value : values) { - ExpressionId id = value.id(); - out.add(value.toAttribute().withName(value.name() + "_" + name).withDataType(agg.dataType()).withId(id)); + out.add(value.toAttribute().withName(value.name() + "_" + name).withDataType(agg.dataType())); } } } @@ -126,6 +130,29 @@ public AttributeSet valuesOutput() { return valueOutput; } + public AttributeMap valuesToLiterals() { + AttributeSet outValues = valuesOutput(); + Map valuesMap = new LinkedHashMap<>(); + + int index = 0; + // for each attribute, associate its value + // take into account while iterating that attributes are a multiplication of actual values + for (Attribute attribute : outValues) { + NamedExpression namedExpression = values.get(index % values.size()); + index++; + // everything should have resolved to an alias + if (namedExpression instanceof Alias) { + valuesMap.put(attribute, Literal.of(((Alias) namedExpression).child())); + } + // fallback - verifier should prevent this + else { + throw new SqlIllegalArgumentException("Unexpected alias", namedExpression); + } + } + + return new AttributeMap<>(valuesMap); + } + @Override public List output() { if (output == null) { @@ -137,6 +164,14 @@ public List output() { return output; } + // Since pivot creates its own columns (and thus aliases) + // remember the backing expressions inside a dedicated aliases map + public AttributeMap aliases() { + // make sure to initialize all expressions + output(); + return aliases; + } + @Override public boolean expressionsResolved() { return column.resolved() && Resolvables.resolved(values) && Resolvables.resolved(aggregates); @@ -163,4 +198,4 @@ public boolean equals(Object obj) { && Objects.equals(aggregates, other.aggregates) && Objects.equals(child(), other.child()); } -} +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/SubQueryAlias.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/SubQueryAlias.java index 980cd0a849a52..dd8fa5bec430e 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/SubQueryAlias.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/SubQueryAlias.java @@ -17,6 +17,7 @@ public class SubQueryAlias extends UnaryPlan { private final String alias; + private List output; public SubQueryAlias(Source source, LogicalPlan child, String alias) { super(source, child); @@ -39,11 +40,13 @@ public String alias() { @Override public List output() { - return (alias == null ? child().output() : + if (output == null) { + output = alias == null ? child().output() : child().output().stream() .map(e -> e.withQualifier(alias)) - .collect(toList()) - ); + .collect(toList()); + } + return output; } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryFolder.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryFolder.java index 8dc9b5b595add..72e4ca380fd33 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryFolder.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryFolder.java @@ -12,29 +12,34 @@ import org.elasticsearch.xpack.sql.expression.Alias; import org.elasticsearch.xpack.sql.expression.Attribute; import org.elasticsearch.xpack.sql.expression.AttributeMap; -import org.elasticsearch.xpack.sql.expression.AttributeSet; import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.ExpressionId; import org.elasticsearch.xpack.sql.expression.Expressions; +import org.elasticsearch.xpack.sql.expression.FieldAttribute; import org.elasticsearch.xpack.sql.expression.Foldables; +import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.expression.Order; +import org.elasticsearch.xpack.sql.expression.ReferenceAttribute; import org.elasticsearch.xpack.sql.expression.function.Function; import org.elasticsearch.xpack.sql.expression.function.Functions; -import org.elasticsearch.xpack.sql.expression.function.ScoreAttribute; +import org.elasticsearch.xpack.sql.expression.function.Score; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; import org.elasticsearch.xpack.sql.expression.function.aggregate.CompoundNumericAggregate; import org.elasticsearch.xpack.sql.expression.function.aggregate.Count; import org.elasticsearch.xpack.sql.expression.function.aggregate.InnerAggregate; import org.elasticsearch.xpack.sql.expression.function.aggregate.TopHits; import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunction; +import org.elasticsearch.xpack.sql.expression.function.grouping.Histogram; import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; -import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunctionAttribute; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeHistogramFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.Year; import org.elasticsearch.xpack.sql.expression.gen.pipeline.AggPathInput; import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; import org.elasticsearch.xpack.sql.expression.gen.pipeline.UnaryPipe; import org.elasticsearch.xpack.sql.expression.gen.processor.Processor; +import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.expression.literal.IntervalYearMonth; +import org.elasticsearch.xpack.sql.expression.literal.Intervals; import org.elasticsearch.xpack.sql.plan.logical.Pivot; import org.elasticsearch.xpack.sql.plan.physical.AggregateExec; import org.elasticsearch.xpack.sql.plan.physical.EsQueryExec; @@ -45,12 +50,15 @@ import org.elasticsearch.xpack.sql.plan.physical.PhysicalPlan; import org.elasticsearch.xpack.sql.plan.physical.PivotExec; import org.elasticsearch.xpack.sql.plan.physical.ProjectExec; -import org.elasticsearch.xpack.sql.planner.QueryTranslator.GroupingContext; import org.elasticsearch.xpack.sql.planner.QueryTranslator.QueryTranslation; import org.elasticsearch.xpack.sql.querydsl.agg.AggFilter; import org.elasticsearch.xpack.sql.querydsl.agg.Aggs; +import org.elasticsearch.xpack.sql.querydsl.agg.GroupByDateHistogram; import org.elasticsearch.xpack.sql.querydsl.agg.GroupByKey; +import org.elasticsearch.xpack.sql.querydsl.agg.GroupByNumericHistogram; +import org.elasticsearch.xpack.sql.querydsl.agg.GroupByValue; import org.elasticsearch.xpack.sql.querydsl.agg.LeafAgg; +import org.elasticsearch.xpack.sql.querydsl.container.AggregateSort; import org.elasticsearch.xpack.sql.querydsl.container.AttributeSort; import org.elasticsearch.xpack.sql.querydsl.container.ComputedRef; import org.elasticsearch.xpack.sql.querydsl.container.GlobalCountRef; @@ -69,18 +77,21 @@ import org.elasticsearch.xpack.sql.rule.RuleExecutor; import org.elasticsearch.xpack.sql.session.EmptyExecutable; import org.elasticsearch.xpack.sql.util.Check; +import org.elasticsearch.xpack.sql.util.DateUtils; +import java.time.Period; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicReference; import static org.elasticsearch.xpack.sql.planner.QueryTranslator.and; import static org.elasticsearch.xpack.sql.planner.QueryTranslator.toAgg; import static org.elasticsearch.xpack.sql.planner.QueryTranslator.toQuery; +import static org.elasticsearch.xpack.sql.type.DataType.DATE; import static org.elasticsearch.xpack.sql.util.CollectionUtils.combine; /** @@ -123,38 +134,26 @@ protected PhysicalPlan rule(ProjectExec project) { EsQueryExec exec = (EsQueryExec) project.child(); QueryContainer queryC = exec.queryContainer(); - Map aliases = new LinkedHashMap<>(queryC.aliases()); + Map aliases = new LinkedHashMap<>(queryC.aliases()); Map processors = new LinkedHashMap<>(queryC.scalarFunctions()); for (NamedExpression pj : project.projections()) { if (pj instanceof Alias) { - Attribute aliasAttr = pj.toAttribute(); + Attribute attr = pj.toAttribute(); Expression e = ((Alias) pj).child(); - if (e instanceof NamedExpression) { - Attribute attr = ((NamedExpression) e).toAttribute(); - aliases.put(aliasAttr.id(), attr); - // add placeholder for each scalar function - if (e instanceof ScalarFunction) { - processors.put(attr, Expressions.pipe(e)); - } - } else { - processors.put(aliasAttr, Expressions.pipe(e)); - } - } - else { - // for named expressions nothing is recorded as these are resolved last - // otherwise 'intermediate' projects might pollute the - // output - if (pj instanceof ScalarFunction) { - ScalarFunction f = (ScalarFunction) pj; - processors.put(f.toAttribute(), Expressions.pipe(f)); + // track all aliases (to determine their reference later on) + aliases.put(attr, e); + + // track scalar pipelines + if (e instanceof ScalarFunction) { + processors.put(attr, ((ScalarFunction) e).asPipe()); } } } QueryContainer clone = new QueryContainer(queryC.query(), queryC.aggs(), queryC.fields(), - new HashMap<>(aliases), + new AttributeMap<>(aliases), queryC.pseudoFunctions(), new AttributeMap<>(processors), queryC.sort(), @@ -214,7 +213,176 @@ private Aggs addPipelineAggs(QueryContainer qContainer, QueryTranslation qt, Fil } } - private static class FoldAggregate extends FoldingRule { + // TODO: remove exceptions from the Folder + static class FoldAggregate extends FoldingRule { + + static class GroupingContext { + final Map groupMap; + final GroupByKey tail; + + GroupingContext(Map groupMap) { + this.groupMap = groupMap; + + GroupByKey lastAgg = null; + for (Entry entry : groupMap.entrySet()) { + lastAgg = entry.getValue(); + } + + tail = lastAgg; + } + + GroupByKey groupFor(Expression exp) { + Integer hash = null; + if (Functions.isAggregate(exp)) { + AggregateFunction f = (AggregateFunction) exp; + // if there's at least one agg in the tree + if (groupMap.isEmpty() == false) { + GroupByKey matchingGroup = null; + // group found - finding the dedicated agg + // TODO: when dealing with expressions inside Aggregation, make sure to extract the field + hash = Integer.valueOf(f.field().hashCode()); + matchingGroup = groupMap.get(hash); + // return matching group or the tail (last group) + return matchingGroup != null ? matchingGroup : tail; + } else { + return null; + } + } + + hash = Integer.valueOf(exp.hashCode()); + return groupMap.get(hash); + } + + @Override + public String toString() { + return groupMap.toString(); + } + } + + /** + * Creates the list of GroupBy keys + */ + static GroupingContext groupBy(List groupings) { + if (groupings.isEmpty() == true) { + return null; + } + + Map aggMap = new LinkedHashMap<>(); + + for (Expression exp : groupings) { + GroupByKey key = null; + + Integer hash = Integer.valueOf(exp.hashCode()); + String aggId = Expressions.id(exp); + + // change analyzed to non non-analyzed attributes + if (exp instanceof FieldAttribute) { + FieldAttribute field = (FieldAttribute) exp; + field = field.exactAttribute(); + key = new GroupByValue(aggId, field.name()); + } + + // handle functions + else if (exp instanceof Function) { + // dates are handled differently because of date histograms + if (exp instanceof DateTimeHistogramFunction) { + DateTimeHistogramFunction dthf = (DateTimeHistogramFunction) exp; + + Expression field = dthf.field(); + if (field instanceof FieldAttribute) { + if (dthf.calendarInterval() != null) { + key = new GroupByDateHistogram(aggId, QueryTranslator.nameOf(exp), dthf.calendarInterval(), dthf.zoneId()); + } else { + key = new GroupByDateHistogram(aggId, QueryTranslator.nameOf(exp), dthf.fixedInterval(), dthf.zoneId()); + } + } + // use scripting for functions + else if (field instanceof Function) { + ScriptTemplate script = ((Function) field).asScript(); + if (dthf.calendarInterval() != null) { + key = new GroupByDateHistogram(aggId, script, dthf.calendarInterval(), dthf.zoneId()); + } else { + key = new GroupByDateHistogram(aggId, script, dthf.fixedInterval(), dthf.zoneId()); + } + } + } + // all other scalar functions become a script + else if (exp instanceof ScalarFunction) { + ScalarFunction sf = (ScalarFunction) exp; + key = new GroupByValue(aggId, sf.asScript()); + } + // histogram + else if (exp instanceof GroupingFunction) { + if (exp instanceof Histogram) { + Histogram h = (Histogram) exp; + Expression field = h.field(); + + // date histogram + if (h.dataType().isDateBased()) { + Object value = h.interval().value(); + // interval of exactly 1 year + if (value instanceof IntervalYearMonth + && ((IntervalYearMonth) value).interval().equals(Period.ofYears(1))) { + String calendarInterval = Year.YEAR_INTERVAL; + + // When the histogram is `INTERVAL '1' YEAR`, the interval used in the ES date_histogram will be + // a calendar_interval with value "1y". All other intervals will be fixed_intervals expressed in ms. + if (field instanceof FieldAttribute) { + key = new GroupByDateHistogram(aggId, QueryTranslator.nameOf(field), calendarInterval, h.zoneId()); + } else if (field instanceof Function) { + key = new GroupByDateHistogram(aggId, ((Function) field).asScript(), calendarInterval, h.zoneId()); + } + } + // typical interval + else { + long intervalAsMillis = Intervals.inMillis(h.interval()); + + // When the histogram in SQL is applied on DATE type instead of DATETIME, the interval + // specified is truncated to the multiple of a day. If the interval specified is less + // than 1 day, then the interval used will be `INTERVAL '1' DAY`. + if (h.dataType() == DATE) { + intervalAsMillis = DateUtils.minDayInterval(intervalAsMillis); + } + + if (field instanceof FieldAttribute) { + key = new GroupByDateHistogram(aggId, QueryTranslator.nameOf(field), intervalAsMillis, h.zoneId()); + } else if (field instanceof Function) { + key = new GroupByDateHistogram(aggId, ((Function) field).asScript(), intervalAsMillis, h.zoneId()); + } + } + } + // numeric histogram + else { + if (field instanceof FieldAttribute) { + key = new GroupByNumericHistogram(aggId, QueryTranslator.nameOf(field), + Foldables.doubleValueOf(h.interval())); + } else if (field instanceof Function) { + key = new GroupByNumericHistogram(aggId, ((Function) field).asScript(), + Foldables.doubleValueOf(h.interval())); + } + } + if (key == null) { + throw new SqlIllegalArgumentException("Unsupported histogram field {}", field); + } + } else { + throw new SqlIllegalArgumentException("Unsupproted grouping function {}", exp); + } + } + // bumped into into an invalid function (which should be caught by the verifier) + else { + throw new SqlIllegalArgumentException("Cannot GROUP BY function {}", exp); + } + } + // catch corner-case + else { + throw new SqlIllegalArgumentException("Cannot GROUP BY {}", exp); + } + + aggMap.put(hash, key); + } + return new GroupingContext(aggMap); + } + @Override protected PhysicalPlan rule(AggregateExec a) { if (a.child() instanceof EsQueryExec) { @@ -225,46 +393,71 @@ protected PhysicalPlan rule(AggregateExec a) { } static EsQueryExec fold(AggregateExec a, EsQueryExec exec) { - // build the group aggregation - // and also collect info about it (since the group columns might be used inside the select) + + QueryContainer queryC = exec.queryContainer(); + + // track aliases defined in the SELECT and used inside GROUP BY + // SELECT x AS a ... GROUP BY a + Map aliasMap = new LinkedHashMap<>(); + for (NamedExpression ne : a.aggregates()) { + if (ne instanceof Alias) { + aliasMap.put(ne.toAttribute(), ((Alias) ne).child()); + } + } + + if (aliasMap.isEmpty() == false) { + Map newAliases = new LinkedHashMap<>(queryC.aliases()); + newAliases.putAll(aliasMap); + queryC = queryC.withAliases(new AttributeMap<>(newAliases)); + } - GroupingContext groupingContext = QueryTranslator.groupBy(a.groupings()); + // build the group aggregation + // NB: any reference in grouping is already "optimized" by its source so there's no need to look for aliases + GroupingContext groupingContext = groupBy(a.groupings()); - QueryContainer queryC = exec.queryContainer(); if (groupingContext != null) { queryC = queryC.addGroups(groupingContext.groupMap.values()); } - Map aliases = new LinkedHashMap<>(); // tracker for compound aggs seen in a group Map compoundAggMap = new LinkedHashMap<>(); // followed by actual aggregates for (NamedExpression ne : a.aggregates()) { - // unwrap alias - it can be - // - an attribute (since we support aliases inside group-by) - // SELECT emp_no ... GROUP BY emp_no + // unwrap alias (since we support aliases declared inside SELECTs to be used by the GROUP BY) + // An alias can point to : + // - field + // SELECT emp_no AS e ... GROUP BY e + // - a function // SELECT YEAR(hire_date) ... GROUP BY YEAR(hire_date) - // - an agg function (typically) + // - an agg function over the grouped field // SELECT COUNT(*), AVG(salary) ... GROUP BY salary; - // - a scalar function, which can be applied on an attribute or aggregate and can require one or multiple inputs + // - a scalar function, which can be applied on a column or aggregate and can require one or multiple inputs // SELECT SIN(emp_no) ... GROUP BY emp_no // SELECT CAST(YEAR(hire_date)) ... GROUP BY YEAR(hire_date) // SELECT CAST(AVG(salary)) ... GROUP BY salary // SELECT AVG(salary) + SIN(MIN(salary)) ... GROUP BY salary - if (ne instanceof Alias || ne instanceof Function) { - Alias as = ne instanceof Alias ? (Alias) ne : null; - Expression child = as != null ? as.child() : ne; + Expression target = ne; - // record aliases in case they are later referred in the tree - if (as != null && as.child() instanceof NamedExpression) { - aliases.put(as.toAttribute().id(), ((NamedExpression) as.child()).toAttribute()); - } + // unwrap aliases since it's the children we are interested in + if (ne instanceof Alias) { + target = ((Alias) ne).child(); + } + + String id = Expressions.id(target); + + // literal + if (target.foldable()) { + queryC = queryC.addColumn(ne.toAttribute()); + } + + // look at functions + else if (target instanceof Function) { // // look first for scalar functions which might wrap the actual grouped target @@ -273,12 +466,14 @@ static EsQueryExec fold(AggregateExec a, EsQueryExec exec) { // ABS(YEAR(field)) GROUP BY YEAR(field) or // ABS(AVG(salary)) ... GROUP BY salary // ) - if (child instanceof ScalarFunction) { - ScalarFunction f = (ScalarFunction) child; + + if (target instanceof ScalarFunction) { + ScalarFunction f = (ScalarFunction) target; Pipe proc = f.asPipe(); final AtomicReference qC = new AtomicReference<>(queryC); + // traverse the pipe to find the mandatory grouping expression proc = proc.transformUp(p -> { // bail out if the def is resolved if (p.resolved()) { @@ -295,6 +490,7 @@ static EsQueryExec fold(AggregateExec a, EsQueryExec exec) { } else { // a scalar function can be used only if has already been mentioned for grouping // (otherwise it is the opposite of grouping) + // normally this case should be caught by the Verifier if (exp instanceof ScalarFunction) { throw new FoldingException(exp, "Scalar function " + exp.toString() + " can be used only if included already in grouping"); @@ -332,77 +528,71 @@ static EsQueryExec fold(AggregateExec a, EsQueryExec exec) { return p; }); - if (!proc.resolved()) { - throw new FoldingException(child, "Cannot find grouping for '{}'", Expressions.name(child)); + if (proc.resolved() == false) { + throw new FoldingException(target, "Cannot find grouping for '{}'", Expressions.name(target)); } // add the computed column - queryC = qC.get().addColumn(new ComputedRef(proc), f.toAttribute()); - - // TODO: is this needed? - // redirect the alias to the scalar group id (changing the id altogether doesn't work it is - // already used in the aggpath) - //aliases.put(as.toAttribute(), sf.toAttribute()); + queryC = qC.get().addColumn(new ComputedRef(proc), id); } + // apply the same logic above (for function inputs) to non-scalar functions with small variations: // instead of adding things as input, add them as full blown column else { GroupByKey matchingGroup = null; if (groupingContext != null) { // is there a group (aggregation) for this expression ? - matchingGroup = groupingContext.groupFor(child); + matchingGroup = groupingContext.groupFor(target); } // attributes can only refer to declared groups - if (child instanceof Attribute) { - Check.notNull(matchingGroup, "Cannot find group [{}]", Expressions.name(child)); - queryC = queryC.addColumn(new GroupByRef(matchingGroup.id(), null, child.dataType().isDateBased()), - ((Attribute) child)); + if (target instanceof Attribute) { + Check.notNull(matchingGroup, "Cannot find group [{}]", Expressions.name(target)); + queryC = queryC.addColumn(new GroupByRef(matchingGroup.id(), null, target.dataType().isDateBased()), id); } // handle histogram - else if (child instanceof GroupingFunction) { - queryC = queryC.addColumn(new GroupByRef(matchingGroup.id(), null, child.dataType().isDateBased()), - ((GroupingFunction) child).toAttribute()); + else if (target instanceof GroupingFunction) { + queryC = queryC.addColumn(new GroupByRef(matchingGroup.id(), null, target.dataType().isDateBased()), id); + } + // handle literal + else if (target.foldable()) { + queryC = queryC.addColumn(ne.toAttribute()); } - else if (child.foldable()) { - queryC = queryC.addColumn(ne.toAttribute()); - } // fallback to regular agg functions else { // the only thing left is agg function - Check.isTrue(Functions.isAggregate(child), "Expected aggregate function inside alias; got [{}]", - child.nodeString()); - AggregateFunction af = (AggregateFunction) child; + Check.isTrue(Functions.isAggregate(target), "Expected aggregate function inside alias; got [{}]", + target.nodeString()); + AggregateFunction af = (AggregateFunction) target; Tuple withAgg = addAggFunction(matchingGroup, af, compoundAggMap, queryC); // make sure to add the inner id (to handle compound aggs) - queryC = withAgg.v1().addColumn(withAgg.v2().context(), af.toAttribute()); + queryC = withAgg.v1().addColumn(withAgg.v2().context(), id); } } - // not an Alias or Function means it's an Attribute so apply the same logic as above - } else { + + } + // not a Function or literal, means its has to be a field or field expression + else { GroupByKey matchingGroup = null; if (groupingContext != null) { - matchingGroup = groupingContext.groupFor(ne); + matchingGroup = groupingContext.groupFor(target); Check.notNull(matchingGroup, "Cannot find group [{}]", Expressions.name(ne)); - queryC = queryC.addColumn(new GroupByRef(matchingGroup.id(), null, ne.dataType().isDateBased()), ne.toAttribute()); + queryC = queryC.addColumn(new GroupByRef(matchingGroup.id(), null, ne.dataType().isDateBased()), id); } - else if (ne.foldable()) { - queryC = queryC.addColumn(ne.toAttribute()); - } + // fallback + else { + throw new SqlIllegalArgumentException("Cannot fold aggregate {}", ne); } } - - if (!aliases.isEmpty()) { - Map newAliases = new LinkedHashMap<>(queryC.aliases()); - newAliases.putAll(aliases); - queryC = queryC.withAliases(new HashMap<>(newAliases)); } + return new EsQueryExec(exec.source(), exec.index(), a.output(), queryC); } private static Tuple addAggFunction(GroupByKey groupingAgg, AggregateFunction f, Map compoundAggMap, QueryContainer queryC) { - String functionId = f.functionId(); + + String functionId = Expressions.id(f); // handle count as a special case agg if (f instanceof Count) { Count c = (Count) f; @@ -422,7 +612,7 @@ private static Tuple addAggFunction(GroupByKey gro pseudoFunctions.put(functionId, groupingAgg); return new Tuple<>(queryC.withPseudoFunctions(pseudoFunctions), new AggPathInput(f, ref)); // COUNT() - } else if (!c.distinct()) { + } else if (c.distinct() == false) { LeafAgg leafAgg = toAgg(functionId, f); AggPathInput a = new AggPathInput(f, new MetricAggRef(leafAgg.id(), "doc_count", "_count", false)); queryC = queryC.with(queryC.aggs().addAgg(leafAgg)); @@ -440,7 +630,7 @@ private static Tuple addAggFunction(GroupByKey gro // the compound agg hasn't been seen before so initialize it if (cAggPath == null) { - LeafAgg leafAgg = toAgg(outer.functionId(), outer); + LeafAgg leafAgg = toAgg(Expressions.id(outer), outer); cAggPath = leafAgg.id(); compoundAggMap.put(outer, cAggPath); // add the agg (without any reference) @@ -480,37 +670,60 @@ protected PhysicalPlan rule(OrderExec plan) { Missing missing = Missing.from(order.nullsPosition()); // check whether sorting is on an group (and thus nested agg) or field - Attribute attr = ((NamedExpression) order.child()).toAttribute(); - // check whether there's an alias (occurs with scalar functions which are not named) - attr = qContainer.aliases().getOrDefault(attr.id(), attr); - GroupByKey group = qContainer.findGroupForAgg(attr); + Expression orderExpression = order.child(); + + // if it's a reference, get the target expression + if (orderExpression instanceof ReferenceAttribute) { + orderExpression = qContainer.aliases().get(orderExpression); + } + String lookup = Expressions.id(orderExpression); + GroupByKey group = qContainer.findGroupForAgg(lookup); // TODO: might need to validate whether the target field or group actually exist if (group != null && group != Aggs.IMPLICIT_GROUP_KEY) { - qContainer = qContainer.updateGroup(group.with(direction)); + // check whether the lookup matches a group + if (group.id().equals(lookup)) { + qContainer = qContainer.updateGroup(group.with(direction)); + } + // else it's a leafAgg + else { + qContainer = qContainer.updateGroup(group.with(direction)); + } } else { // scalar functions typically require script ordering - if (attr instanceof ScalarFunctionAttribute) { - ScalarFunctionAttribute sfa = (ScalarFunctionAttribute) attr; + if (orderExpression instanceof ScalarFunction) { + ScalarFunction sf = (ScalarFunction) orderExpression; // is there an expression to order by? - if (sfa.orderBy() != null) { - if (sfa.orderBy() instanceof NamedExpression) { - Attribute at = ((NamedExpression) sfa.orderBy()).toAttribute(); - at = qContainer.aliases().getOrDefault(at.id(), at); - qContainer = qContainer.addSort(new AttributeSort(at, direction, missing)); - } else if (!sfa.orderBy().foldable()) { + if (sf.orderBy() != null) { + Expression orderBy = sf.orderBy(); + if (orderBy instanceof NamedExpression) { + orderBy = qContainer.aliases().getOrDefault(orderBy, orderBy); + qContainer = qContainer + .addSort(new AttributeSort(((NamedExpression) orderBy).toAttribute(), direction, missing)); + } else if (orderBy.foldable() == false) { // ignore constant - throw new PlanningException("does not know how to order by expression {}", sfa.orderBy()); + throw new PlanningException("does not know how to order by expression {}", orderBy); } } else { // nope, use scripted sorting - qContainer = qContainer.addSort(new ScriptSort(sfa.script(), direction, missing)); + qContainer = qContainer.addSort(new ScriptSort(sf.asScript(), direction, missing)); } - } else if (attr instanceof ScoreAttribute) { + } + // score + else if (orderExpression instanceof Score) { qContainer = qContainer.addSort(new ScoreSort(direction, missing)); + } + // field + else if (orderExpression instanceof FieldAttribute) { + qContainer = qContainer.addSort(new AttributeSort((FieldAttribute) orderExpression, direction, missing)); + } + // agg function + else if (orderExpression instanceof AggregateFunction) { + qContainer = qContainer.addSort(new AggregateSort((AggregateFunction) orderExpression, direction, missing)); } else { - qContainer = qContainer.addSort(new AttributeSort(attr, direction, missing)); + // unknown + throw new SqlIllegalArgumentException("unsupported sorting expression {}", orderExpression); } } } @@ -573,21 +786,22 @@ protected PhysicalPlan rule(PivotExec plan) { // due to the Pivot structure - the column is the last entry in the grouping set QueryContainer query = fold.queryContainer(); - List> fields = new ArrayList<>(query.fields()); + List> fields = new ArrayList<>(query.fields()); int startingIndex = fields.size() - p.aggregates().size() - 1; // pivot grouping - Tuple groupTuple = fields.remove(startingIndex); - AttributeSet valuesOutput = plan.pivot().valuesOutput(); + Tuple groupTuple = fields.remove(startingIndex); + AttributeMap values = p.valuesToLiterals(); for (int i = startingIndex; i < fields.size(); i++) { - Tuple tuple = fields.remove(i); - for (Attribute attribute : valuesOutput) { - fields.add(new Tuple<>(new PivotColumnRef(groupTuple.v1(), tuple.v1(), attribute.fold()), attribute.id())); + Tuple tuple = fields.remove(i); + for (Map.Entry entry : values.entrySet()) { + fields.add(new Tuple<>( + new PivotColumnRef(groupTuple.v1(), tuple.v1(), entry.getValue().value()), Expressions.id(entry.getKey()))); } - i += valuesOutput.size(); + i += values.size(); } - return fold.with(new QueryContainer(query.query(), query.aggs(), + return fold.with(new QueryContainer(query.query(), query.aggs(), fields, query.aliases(), query.pseudoFunctions(), @@ -596,7 +810,7 @@ protected PhysicalPlan rule(PivotExec plan) { query.limit(), query.shouldTrackHits(), query.shouldIncludeFrozen(), - valuesOutput.size())); + values.size())); } return plan; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryTranslator.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryTranslator.java index 4fbcc76ff8220..149999a880214 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryTranslator.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryTranslator.java @@ -10,15 +10,12 @@ import org.elasticsearch.geometry.Point; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; -import org.elasticsearch.xpack.sql.expression.Attribute; import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.Expressions; import org.elasticsearch.xpack.sql.expression.FieldAttribute; -import org.elasticsearch.xpack.sql.expression.Foldables; import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.expression.function.Function; -import org.elasticsearch.xpack.sql.expression.function.Functions; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; import org.elasticsearch.xpack.sql.expression.function.aggregate.Avg; import org.elasticsearch.xpack.sql.expression.function.aggregate.CompoundNumericAggregate; @@ -35,17 +32,11 @@ import org.elasticsearch.xpack.sql.expression.function.aggregate.Stats; import org.elasticsearch.xpack.sql.expression.function.aggregate.Sum; import org.elasticsearch.xpack.sql.expression.function.aggregate.TopHits; -import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunction; -import org.elasticsearch.xpack.sql.expression.function.grouping.Histogram; import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFunction; -import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeHistogramFunction; -import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.Year; import org.elasticsearch.xpack.sql.expression.function.scalar.geo.GeoShape; import org.elasticsearch.xpack.sql.expression.function.scalar.geo.StDistance; import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; -import org.elasticsearch.xpack.sql.expression.literal.IntervalYearMonth; -import org.elasticsearch.xpack.sql.expression.literal.Intervals; import org.elasticsearch.xpack.sql.expression.predicate.Range; import org.elasticsearch.xpack.sql.expression.predicate.fulltext.MatchQueryPredicate; import org.elasticsearch.xpack.sql.expression.predicate.fulltext.MultiMatchQueryPredicate; @@ -74,10 +65,6 @@ import org.elasticsearch.xpack.sql.querydsl.agg.CardinalityAgg; import org.elasticsearch.xpack.sql.querydsl.agg.ExtendedStatsAgg; import org.elasticsearch.xpack.sql.querydsl.agg.FilterExistsAgg; -import org.elasticsearch.xpack.sql.querydsl.agg.GroupByDateHistogram; -import org.elasticsearch.xpack.sql.querydsl.agg.GroupByKey; -import org.elasticsearch.xpack.sql.querydsl.agg.GroupByNumericHistogram; -import org.elasticsearch.xpack.sql.querydsl.agg.GroupByValue; import org.elasticsearch.xpack.sql.querydsl.agg.LeafAgg; import org.elasticsearch.xpack.sql.querydsl.agg.MatrixStatsAgg; import org.elasticsearch.xpack.sql.querydsl.agg.MaxAgg; @@ -106,25 +93,20 @@ import org.elasticsearch.xpack.sql.querydsl.query.WildcardQuery; import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.util.Check; -import org.elasticsearch.xpack.sql.util.DateUtils; import org.elasticsearch.xpack.sql.util.Holder; import org.elasticsearch.xpack.sql.util.ReflectionUtils; import java.time.OffsetTime; -import java.time.Period; import java.time.ZonedDateTime; import java.time.temporal.TemporalAccessor; import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import java.util.function.Supplier; import static java.util.Collections.singletonList; +import static org.elasticsearch.xpack.sql.expression.Expressions.id; import static org.elasticsearch.xpack.sql.expression.Foldables.doubleValuesOf; import static org.elasticsearch.xpack.sql.expression.Foldables.valueOf; -import static org.elasticsearch.xpack.sql.type.DataType.DATE; final class QueryTranslator { @@ -208,174 +190,6 @@ static LeafAgg toAgg(String id, Function f) { throw new SqlIllegalArgumentException("Don't know how to translate {} {}", f.nodeName(), f); } - static class GroupingContext { - final Map groupMap; - final GroupByKey tail; - - GroupingContext(Map groupMap) { - this.groupMap = groupMap; - - GroupByKey lastAgg = null; - for (Entry entry : groupMap.entrySet()) { - lastAgg = entry.getValue(); - } - - tail = lastAgg; - } - - GroupByKey groupFor(Expression exp) { - if (Functions.isAggregate(exp)) { - AggregateFunction f = (AggregateFunction) exp; - // if there's at least one agg in the tree - if (!groupMap.isEmpty()) { - GroupByKey matchingGroup = null; - // group found - finding the dedicated agg - if (f.field() instanceof NamedExpression) { - matchingGroup = groupMap.get(f.field()); - } - // return matching group or the tail (last group) - return matchingGroup != null ? matchingGroup : tail; - } - else { - return null; - } - } - if (exp instanceof NamedExpression) { - return groupMap.get(exp); - } - throw new SqlIllegalArgumentException("Don't know how to find group for expression {}", exp); - } - - @Override - public String toString() { - return groupMap.toString(); - } - } - - /** - * Creates the list of GroupBy keys - */ - static GroupingContext groupBy(List groupings) { - if (groupings.isEmpty()) { - return null; - } - - Map aggMap = new LinkedHashMap<>(); - - for (Expression exp : groupings) { - GroupByKey key = null; - NamedExpression id; - String aggId; - - if (exp instanceof NamedExpression) { - NamedExpression ne = (NamedExpression) exp; - - id = ne; - aggId = ne.id().toString(); - - // change analyzed to non non-analyzed attributes - if (exp instanceof FieldAttribute) { - ne = ((FieldAttribute) exp).exactAttribute(); - } - - // handle functions differently - if (exp instanceof Function) { - // dates are handled differently because of date histograms - if (exp instanceof DateTimeHistogramFunction) { - DateTimeHistogramFunction dthf = (DateTimeHistogramFunction) exp; - Expression field = dthf.field(); - if (field instanceof FieldAttribute) { - if (dthf.calendarInterval() != null) { - key = new GroupByDateHistogram(aggId, nameOf(field), dthf.calendarInterval(), dthf.zoneId()); - } else { - key = new GroupByDateHistogram(aggId, nameOf(field), dthf.fixedInterval(), dthf.zoneId()); - } - } else if (field instanceof Function) { - ScriptTemplate script = ((Function) field).asScript(); - if (dthf.calendarInterval() != null) { - key = new GroupByDateHistogram(aggId, script, dthf.calendarInterval(), dthf.zoneId()); - } else { - key = new GroupByDateHistogram(aggId, script, dthf.fixedInterval(), dthf.zoneId()); - } - } - } - // all other scalar functions become a script - else if (exp instanceof ScalarFunction) { - ScalarFunction sf = (ScalarFunction) exp; - key = new GroupByValue(aggId, sf.asScript()); - } - // histogram - else if (exp instanceof GroupingFunction) { - if (exp instanceof Histogram) { - Histogram h = (Histogram) exp; - Expression field = h.field(); - - // date histogram - if (h.dataType().isDateBased()) { - Object value = h.interval().value(); - if (value instanceof IntervalYearMonth - && ((IntervalYearMonth) value).interval().equals(Period.of(1, 0, 0))) { - String calendarInterval = Year.YEAR_INTERVAL; - - // When the histogram is `INTERVAL '1' YEAR`, the interval used in the ES date_histogram will be - // a calendar_interval with value "1y". All other intervals will be fixed_intervals expressed in ms. - if (field instanceof FieldAttribute) { - key = new GroupByDateHistogram(aggId, nameOf(field), calendarInterval, h.zoneId()); - } else if (field instanceof Function) { - key = new GroupByDateHistogram(aggId, ((Function) field).asScript(), calendarInterval, h.zoneId()); - } - } else { - long intervalAsMillis = Intervals.inMillis(h.interval()); - - // When the histogram in SQL is applied on DATE type instead of DATETIME, the interval - // specified is truncated to the multiple of a day. If the interval specified is less - // than 1 day, then the interval used will be `INTERVAL '1' DAY`. - if (h.dataType() == DATE) { - intervalAsMillis = DateUtils.minDayInterval(intervalAsMillis); - } - - if (field instanceof FieldAttribute) { - key = new GroupByDateHistogram(aggId, nameOf(field), intervalAsMillis, h.zoneId()); - } else if (field instanceof Function) { - key = new GroupByDateHistogram(aggId, ((Function) field).asScript(), intervalAsMillis, h.zoneId()); - } - } - } - // numeric histogram - else { - if (field instanceof FieldAttribute) { - key = new GroupByNumericHistogram(aggId, nameOf(field), Foldables.doubleValueOf(h.interval())); - } else if (field instanceof Function) { - key = new GroupByNumericHistogram(aggId, ((Function) field).asScript(), - Foldables.doubleValueOf(h.interval())); - } - } - if (key == null) { - throw new SqlIllegalArgumentException("Unsupported histogram field {}", field); - } - } - else { - throw new SqlIllegalArgumentException("Unsupproted grouping function {}", exp); - } - } - // bumped into into an invalid function (which should be caught by the verifier) - else { - throw new SqlIllegalArgumentException("Cannot GROUP BY function {}", exp); - } - } - else { - key = new GroupByValue(aggId, ne.name()); - } - } - else { - throw new SqlIllegalArgumentException("Don't know how to group on {}", exp.nodeString()); - } - - aggMap.put(id, key); - } - return new GroupingContext(aggMap); - } - static QueryTranslation and(Source source, QueryTranslation left, QueryTranslation right) { Check.isTrue(left != null || right != null, "Both expressions are null"); if (left == null) { @@ -464,17 +278,9 @@ static String nameOf(Expression e) { if (e instanceof NamedExpression) { return ((NamedExpression) e).name(); } - if (e instanceof Literal) { - return String.valueOf(e.fold()); - } - throw new SqlIllegalArgumentException("Cannot determine name for {}", e); - } - - static String idOf(Expression e) { - if (e instanceof NamedExpression) { - return ((NamedExpression) e).id().toString(); + else { + return e.sourceText(); } - throw new SqlIllegalArgumentException("Cannot determine id for {}", e); } static String dateFormat(Expression e) { @@ -525,7 +331,7 @@ protected QueryTranslation asQuery(RegexMatch e, boolean onAggs) { if (e.field() instanceof FieldAttribute) { targetFieldName = nameOf(((FieldAttribute) e.field()).exactAttribute()); } else { - throw new SqlIllegalArgumentException("Scalar function [{}] not allowed (yet) as argument for " + e.functionName(), + throw new SqlIllegalArgumentException("Scalar function [{}] not allowed (yet) as argument for " + e.sourceText(), Expressions.name(e.field())); } @@ -590,7 +396,7 @@ protected QueryTranslation asQuery(Not not, boolean onAggs) { AggFilter aggFilter = null; if (onAggs) { - aggFilter = new AggFilter(not.id().toString(), not.asScript()); + aggFilter = new AggFilter(id(not), not.asScript()); } else { Expression e = not.field(); Query wrappedQuery = toQuery(not.field(), false).query; @@ -616,7 +422,7 @@ protected QueryTranslation asQuery(IsNotNull isNotNull, boolean onAggs) { AggFilter aggFilter = null; if (onAggs) { - aggFilter = new AggFilter(isNotNull.id().toString(), isNotNull.asScript()); + aggFilter = new AggFilter(id(isNotNull), isNotNull.asScript()); } else { Query q = null; if (isNotNull.field() instanceof FieldAttribute) { @@ -640,7 +446,7 @@ protected QueryTranslation asQuery(IsNull isNull, boolean onAggs) { AggFilter aggFilter = null; if (onAggs) { - aggFilter = new AggFilter(isNull.id().toString(), isNull.asScript()); + aggFilter = new AggFilter(id(isNull), isNull.asScript()); } else { Query q = null; if (isNull.field() instanceof FieldAttribute) { @@ -667,30 +473,18 @@ protected QueryTranslation asQuery(BinaryComparison bc, boolean onAggs) { bc.right().sourceLocation().getLineNumber(), bc.right().sourceLocation().getColumnNumber(), Expressions.name(bc.right()), bc.symbol()); - if (bc.left() instanceof NamedExpression) { - NamedExpression ne = (NamedExpression) bc.left(); - - Query query = null; - AggFilter aggFilter = null; + Query query = null; + AggFilter aggFilter = null; - Attribute at = ne.toAttribute(); - // - // Agg context means HAVING -> PipelineAggs - // - if (onAggs) { - aggFilter = new AggFilter(at.id().toString(), bc.asScript()); - } - else { - query = handleQuery(bc, ne, () -> translateQuery(bc)); - } - return new QueryTranslation(query, aggFilter); - } // - // if the code gets here it's a bug + // Agg context means HAVING -> PipelineAggs // - else { - throw new SqlIllegalArgumentException("No idea how to translate " + bc.left()); + if (onAggs) { + aggFilter = new AggFilter(id(bc.left()), bc.asScript()); + } else { + query = handleQuery(bc, bc.left(), () -> translateQuery(bc)); } + return new QueryTranslation(query, aggFilter); } private static Query translateQuery(BinaryComparison bc) { @@ -778,39 +572,28 @@ static class InComparisons extends ExpressionTranslator { @Override protected QueryTranslation asQuery(In in, boolean onAggs) { - if (in.value() instanceof NamedExpression) { - NamedExpression ne = (NamedExpression) in.value(); - - Query query = null; - AggFilter aggFilter = null; + Query query = null; + AggFilter aggFilter = null; - Attribute at = ne.toAttribute(); - // - // Agg context means HAVING -> PipelineAggs - // - if (onAggs) { - aggFilter = new AggFilter(at.id().toString(), in.asScript()); - } - else { - Query q = null; - if (in.value() instanceof FieldAttribute) { - FieldAttribute fa = (FieldAttribute) in.value(); - // equality should always be against an exact match (which is important for strings) - q = new TermsQuery(in.source(), fa.exactAttribute().name(), in.list()); - } else { - q = new ScriptQuery(in.source(), in.asScript()); - } - Query qu = q; - query = handleQuery(in, ne, () -> qu); - } - return new QueryTranslation(query, aggFilter); - } // - // if the code gets here it's a bug + // Agg context means HAVING -> PipelineAggs // + if (onAggs) { + aggFilter = new AggFilter(id(in.value()), in.asScript()); + } else { - throw new SqlIllegalArgumentException("No idea how to translate " + in.value()); + Query q = null; + if (in.value() instanceof FieldAttribute) { + FieldAttribute fa = (FieldAttribute) in.value(); + // equality should always be against an exact match (which is important for strings) + q = new TermsQuery(in.source(), fa.exactAttribute().name(), in.list()); + } else { + q = new ScriptQuery(in.source(), in.asScript()); + } + Query qu = q; + query = handleQuery(in, in.value(), () -> qu); } + return new QueryTranslation(query, aggFilter); } } @@ -820,53 +603,48 @@ static class Ranges extends ExpressionTranslator { protected QueryTranslation asQuery(Range r, boolean onAggs) { Expression e = r.value(); - if (e instanceof NamedExpression) { - Query query = null; - AggFilter aggFilter = null; + Query query = null; + AggFilter aggFilter = null; - // - // Agg context means HAVING -> PipelineAggs - // - Attribute at = ((NamedExpression) e).toAttribute(); + // + // Agg context means HAVING -> PipelineAggs + // + if (onAggs) { + aggFilter = new AggFilter(id(e), r.asScript()); + } else { - if (onAggs) { - aggFilter = new AggFilter(at.id().toString(), r.asScript()); - } else { - Holder lower = new Holder<>(valueOf(r.lower())); - Holder upper = new Holder<>(valueOf(r.upper())); - Holder format = new Holder<>(dateFormat(r.value())); - - // for a date constant comparison, we need to use a format for the date, to make sure that the format is the same - // no matter the timezone provided by the user - if (format.get() == null) { - DateFormatter formatter = null; - if (lower.get() instanceof ZonedDateTime || upper.get() instanceof ZonedDateTime) { - formatter = DateFormatter.forPattern(DATE_FORMAT); - } else if (lower.get() instanceof OffsetTime || upper.get() instanceof OffsetTime) { - formatter = DateFormatter.forPattern(TIME_FORMAT); + Holder lower = new Holder<>(valueOf(r.lower())); + Holder upper = new Holder<>(valueOf(r.upper())); + Holder format = new Holder<>(dateFormat(r.value())); + + // for a date constant comparison, we need to use a format for the date, to make sure that the format is the same + // no matter the timezone provided by the user + if (format.get() == null) { + DateFormatter formatter = null; + if (lower.get() instanceof ZonedDateTime || upper.get() instanceof ZonedDateTime) { + formatter = DateFormatter.forPattern(DATE_FORMAT); + } else if (lower.get() instanceof OffsetTime || upper.get() instanceof OffsetTime) { + formatter = DateFormatter.forPattern(TIME_FORMAT); + } + if (formatter != null) { + // RangeQueryBuilder accepts an Object as its parameter, but it will call .toString() on the ZonedDateTime + // instance which can have a slightly different format depending on the ZoneId used to create the ZonedDateTime + // Since RangeQueryBuilder can handle date as String as well, we'll format it as String and provide the format. + if (lower.get() instanceof ZonedDateTime || lower.get() instanceof OffsetTime) { + lower.set(formatter.format((TemporalAccessor) lower.get())); } - if (formatter != null) { - // RangeQueryBuilder accepts an Object as its parameter, but it will call .toString() on the ZonedDateTime - // instance which can have a slightly different format depending on the ZoneId used to create the ZonedDateTime - // Since RangeQueryBuilder can handle date as String as well, we'll format it as String and provide the format. - if (lower.get() instanceof ZonedDateTime || lower.get() instanceof OffsetTime) { - lower.set(formatter.format((TemporalAccessor) lower.get())); - } - if (upper.get() instanceof ZonedDateTime || upper.get() instanceof OffsetTime) { - upper.set(formatter.format((TemporalAccessor) upper.get())); - } - format.set(formatter.pattern()); + if (upper.get() instanceof ZonedDateTime || upper.get() instanceof OffsetTime) { + upper.set(formatter.format((TemporalAccessor) upper.get())); } + format.set(formatter.pattern()); } - - query = handleQuery(r, r.value(), - () -> new RangeQuery(r.source(), nameOf(r.value()), lower.get(), r.includeLower(), - upper.get(), r.includeUpper(), format.get())); } - return new QueryTranslation(query, aggFilter); - } else { - throw new SqlIllegalArgumentException("No idea how to translate " + e); + + query = handleQuery(r, r.value(), + () -> new RangeQuery(r.source(), nameOf(r.value()), lower.get(), r.includeLower(), upper.get(), r.includeUpper(), + format.get())); } + return new QueryTranslation(query, aggFilter); } } @@ -880,7 +658,7 @@ protected QueryTranslation asQuery(ScalarFunction f, boolean onAggs) { AggFilter aggFilter = null; if (onAggs) { - aggFilter = new AggFilter(f.id().toString(), script); + aggFilter = new AggFilter(id(f), script); } else { query = handleQuery(f, f, () -> new ScriptQuery(f.source(), script)); } @@ -1086,4 +864,4 @@ protected static Query wrapIfNested(Query query, Expression exp) { return query; } } -} +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/Aggs.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/Aggs.java index 632eb729936fd..94f854c29f0b8 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/Aggs.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/Aggs.java @@ -10,8 +10,6 @@ import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregationBuilder; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; -import org.elasticsearch.xpack.sql.expression.Attribute; -import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunctionAttribute; import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; import org.elasticsearch.xpack.sql.querydsl.container.Sort.Direction; import org.elasticsearch.xpack.sql.util.StringUtils; @@ -123,23 +121,16 @@ public Aggs addAgg(PipelineAgg pipelineAgg) { return new Aggs(groups, simpleAggs, combine(pipelineAggs, pipelineAgg)); } - public GroupByKey findGroupForAgg(Attribute attr) { - String id = attr.id().toString(); + public GroupByKey findGroupForAgg(String groupOrAggId) { for (GroupByKey group : this.groups) { - if (id.equals(group.id())) { + if (groupOrAggId.equals(group.id())) { return group; } - if (attr instanceof ScalarFunctionAttribute) { - ScalarFunctionAttribute sfa = (ScalarFunctionAttribute) attr; - if (group.script() != null && group.script().equals(sfa.script())) { - return group; - } - } } // maybe it's the default group agg ? for (Agg agg : simpleAggs) { - if (id.equals(agg.id())) { + if (groupOrAggId.equals(agg.id())) { return IMPLICIT_GROUP_KEY; } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/AggregateSort.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/AggregateSort.java new file mode 100644 index 0000000000000..966f5c5079664 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/AggregateSort.java @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.sql.querydsl.container; + +import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; + +import java.util.Objects; + +public class AggregateSort extends Sort { + + private final AggregateFunction agg; + + public AggregateSort(AggregateFunction agg, Direction direction, Missing missing) { + super(direction, missing); + this.agg = agg; + } + + public AggregateFunction agg() { + return agg; + } + + @Override + public int hashCode() { + return Objects.hash(agg, direction(), missing()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + AggregateSort other = (AggregateSort) obj; + return Objects.equals(direction(), other.direction()) + && Objects.equals(missing(), other.missing()) + && Objects.equals(agg, other.agg); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainer.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainer.java index 3dd1a2ac10834..2e388f94af3e5 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainer.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainer.java @@ -16,13 +16,15 @@ import org.elasticsearch.xpack.sql.execution.search.SourceGenerator; import org.elasticsearch.xpack.sql.expression.Attribute; import org.elasticsearch.xpack.sql.expression.AttributeMap; -import org.elasticsearch.xpack.sql.expression.ExpressionId; +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.Expressions; import org.elasticsearch.xpack.sql.expression.FieldAttribute; -import org.elasticsearch.xpack.sql.expression.LiteralAttribute; -import org.elasticsearch.xpack.sql.expression.function.ScoreAttribute; -import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; -import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunctionAttribute; +import org.elasticsearch.xpack.sql.expression.function.Score; +import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.ConstantInput; import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.ScorePipe; import org.elasticsearch.xpack.sql.querydsl.agg.Aggs; import org.elasticsearch.xpack.sql.querydsl.agg.GroupByKey; import org.elasticsearch.xpack.sql.querydsl.agg.LeafAgg; @@ -38,7 +40,6 @@ import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -67,11 +68,11 @@ public class QueryContainer { // for example in case of grouping or custom sorting, the response has extra columns // that is filtered before getting to the client - // the list contains both the field extraction and the id of its associated attribute (for custom sorting) - private final List> fields; + // the list contains both the field extraction and its id (for custom sorting) + private final List> fields; - // aliases (maps an alias to its actual resolved attribute) - private final Map aliases; + // aliases found in the tree + private final AttributeMap aliases; // pseudo functions (like count) - that are 'extracted' from other aggs private final Map pseudoFunctions; @@ -90,6 +91,9 @@ public class QueryContainer { // computed private Boolean aggsOnly; private Boolean customSort; + // associate Attributes with aliased FieldAttributes (since they map directly to ES fields) + private Map fieldAlias; + public QueryContainer() { this(null, null, null, null, null, null, null, -1, false, false, -1); @@ -97,9 +101,8 @@ public QueryContainer() { public QueryContainer(Query query, Aggs aggs, - List> fields, - Map aliases, + List> fields, + AttributeMap aliases, Map pseudoFunctions, AttributeMap scalarFunctions, Set sort, @@ -110,7 +113,7 @@ public QueryContainer(Query query, this.query = query; this.aggs = aggs == null ? Aggs.EMPTY : aggs; this.fields = fields == null || fields.isEmpty() ? emptyList() : fields; - this.aliases = aliases == null || aliases.isEmpty() ? Collections.emptyMap() : aliases; + this.aliases = aliases == null || aliases.isEmpty() ? AttributeMap.emptyAttributeMap() : aliases; this.pseudoFunctions = pseudoFunctions == null || pseudoFunctions.isEmpty() ? emptyMap() : pseudoFunctions; this.scalarFunctions = scalarFunctions == null || scalarFunctions.isEmpty() ? AttributeMap.emptyAttributeMap() : scalarFunctions; this.sort = sort == null || sort.isEmpty() ? emptySet() : sort; @@ -136,31 +139,30 @@ public List> sortingColumns() { for (Sort s : sort) { Tuple tuple = new Tuple<>(Integer.valueOf(-1), null); - if (s instanceof AttributeSort) { - AttributeSort as = (AttributeSort) s; + if (s instanceof AggregateSort) { + AggregateSort as = (AggregateSort) s; // find the relevant column of each aggregate function - if (as.attribute() instanceof AggregateFunctionAttribute) { - aggSort = true; - AggregateFunctionAttribute afa = (AggregateFunctionAttribute) as.attribute(); - afa = (AggregateFunctionAttribute) aliases.getOrDefault(afa.innerId(), afa); - int atIndex = -1; - for (int i = 0; i < fields.size(); i++) { - Tuple field = fields.get(i); - if (field.v2().equals(afa.innerId())) { - atIndex = i; - break; - } - } + AggregateFunction af = as.agg(); - if (atIndex == -1) { - throw new SqlIllegalArgumentException("Cannot find backing column for ordering aggregation [{}]", afa.name()); - } - // assemble a comparator for it - Comparator comp = s.direction() == Sort.Direction.ASC ? Comparator.naturalOrder() : Comparator.reverseOrder(); - comp = s.missing() == Sort.Missing.FIRST ? Comparator.nullsFirst(comp) : Comparator.nullsLast(comp); + aggSort = true; + int atIndex = -1; + String id = Expressions.id(af); - tuple = new Tuple<>(Integer.valueOf(atIndex), comp); + for (int i = 0; i < fields.size(); i++) { + Tuple field = fields.get(i); + if (field.v2().equals(id)) { + atIndex = i; + break; + } + } + if (atIndex == -1) { + throw new SqlIllegalArgumentException("Cannot find backing column for ordering aggregation [{}]", s); } + // assemble a comparator for it + Comparator comp = s.direction() == Sort.Direction.ASC ? Comparator.naturalOrder() : Comparator.reverseOrder(); + comp = s.missing() == Sort.Missing.FIRST ? Comparator.nullsFirst(comp) : Comparator.nullsLast(comp); + + tuple = new Tuple<>(Integer.valueOf(atIndex), comp); } sortingColumns.add(tuple); } @@ -179,19 +181,20 @@ public List> sortingColumns() { */ public BitSet columnMask(List columns) { BitSet mask = new BitSet(fields.size()); + aliasName(columns.get(0)); + for (Attribute column : columns) { - Attribute alias = aliases.get(column.id()); + Expression expression = aliases.getOrDefault(column, column); + // find the column index + String id = Expressions.id(expression); int index = -1; - ExpressionId id = column instanceof AggregateFunctionAttribute ? ((AggregateFunctionAttribute) column).innerId() : column.id(); - ExpressionId aliasId = alias != null ? (alias instanceof AggregateFunctionAttribute ? ((AggregateFunctionAttribute) alias) - .innerId() : alias.id()) : null; for (int i = 0; i < fields.size(); i++) { - Tuple tuple = fields.get(i); + Tuple tuple = fields.get(i); // if the index is already set there is a collision, // so continue searching for the other tuple with the same id - if (mask.get(i)==false && (tuple.v2().equals(id) || (aliasId != null && tuple.v2().equals(aliasId)))) { + if (mask.get(i) == false && tuple.v2().equals(id)) { index = i; break; } @@ -214,11 +217,11 @@ public Aggs aggs() { return aggs; } - public List> fields() { + public List> fields() { return fields; } - public Map aliases() { + public AttributeMap aliases() { return aliases; } @@ -267,12 +270,7 @@ public QueryContainer with(Query q) { minPageSize); } - public QueryContainer withFields(List> f) { - return new QueryContainer(query, aggs, f, aliases, pseudoFunctions, scalarFunctions, sort, limit, trackHits, includeFrozen, - minPageSize); - } - - public QueryContainer withAliases(Map a) { + public QueryContainer withAliases(AttributeMap a) { return new QueryContainer(query, aggs, fields, a, pseudoFunctions, scalarFunctions, sort, limit, trackHits, includeFrozen, minPageSize); } @@ -313,7 +311,16 @@ public QueryContainer addSort(Sort sortable) { } private String aliasName(Attribute attr) { - return aliases.getOrDefault(attr.id(), attr).name(); + if (fieldAlias == null) { + fieldAlias = new LinkedHashMap<>(); + for (Map.Entry entry : aliases.entrySet()) { + if (entry.getValue() instanceof FieldAttribute) { + fieldAlias.put(entry.getKey(), (FieldAttribute) entry.getValue()); + } + } + } + FieldAttribute fa = fieldAlias.get(attr); + return fa != null ? fa.name() : attr.name(); } // @@ -397,17 +404,8 @@ static Query rewriteToContainNestedField(@Nullable Query query, Source source, S } // replace function/operators's input with references - private Tuple resolvedTreeComputingRef(ScalarFunctionAttribute ta) { - Attribute attribute = aliases.getOrDefault(ta.id(), ta); - Pipe proc = scalarFunctions.get(attribute); - - // check the attribute itself - if (proc == null) { - if (attribute instanceof ScalarFunctionAttribute) { - ta = (ScalarFunctionAttribute) attribute; - } - proc = ta.asPipe(); - } + private Tuple resolvedTreeComputingRef(ScalarFunction function, Attribute attr) { + Pipe proc = scalarFunctions.computeIfAbsent(attr, v -> function.asPipe()); // find the processor inputs (Attributes) and convert them into references // no need to promote them to the top since the container doesn't have to be aware @@ -420,8 +418,7 @@ private QueryAttributeResolver(QueryContainer container) { @Override public FieldExtraction resolve(Attribute attribute) { - Attribute attr = aliases.getOrDefault(attribute.id(), attribute); - Tuple ref = container.toReference(attr); + Tuple ref = container.asFieldExtraction(attribute); container = ref.v1(); return ref.v2(); } @@ -430,42 +427,55 @@ public FieldExtraction resolve(Attribute attribute) { proc = proc.resolveAttributes(resolver); QueryContainer qContainer = resolver.container; - // update proc - Map procs = new LinkedHashMap<>(qContainer.scalarFunctions()); - procs.put(attribute, proc); - qContainer = qContainer.withScalarProcessors(new AttributeMap<>(procs)); + // update proc (if needed) + if (qContainer.scalarFunctions().size() != scalarFunctions.size()) { + Map procs = new LinkedHashMap<>(qContainer.scalarFunctions()); + procs.put(attr, proc); + qContainer = qContainer.withScalarProcessors(new AttributeMap<>(procs)); + } + return new Tuple<>(qContainer, new ComputedRef(proc)); } public QueryContainer addColumn(Attribute attr) { - Tuple tuple = toReference(attr); - return tuple.v1().addColumn(tuple.v2(), attr); + Expression expression = aliases.getOrDefault(attr, attr); + Tuple tuple = asFieldExtraction(attr); + return tuple.v1().addColumn(tuple.v2(), Expressions.id(expression)); } - private Tuple toReference(Attribute attr) { - if (attr instanceof FieldAttribute) { - FieldAttribute fa = (FieldAttribute) attr; + private Tuple asFieldExtraction(Attribute attr) { + // resolve it Expression + Expression expression = aliases.getOrDefault(attr, attr); + + if (expression instanceof FieldAttribute) { + FieldAttribute fa = (FieldAttribute) expression; if (fa.isNested()) { return nestedHitFieldRef(fa); } else { return new Tuple<>(this, topHitFieldRef(fa)); } } - if (attr instanceof ScalarFunctionAttribute) { - return resolvedTreeComputingRef((ScalarFunctionAttribute) attr); + + if (expression == null) { + throw new SqlIllegalArgumentException("Unknown output attribute {}", attr); + } + + if (expression.foldable()) { + return new Tuple<>(this, new ComputedRef(new ConstantInput(expression.source(), expression, expression.fold()))); } - if (attr instanceof LiteralAttribute) { - return new Tuple<>(this, new ComputedRef(((LiteralAttribute) attr).asPipe())); + + if (expression instanceof Score) { + return new Tuple<>(this, new ComputedRef(new ScorePipe(expression.source(), expression))); } - if (attr instanceof ScoreAttribute) { - return new Tuple<>(this, new ComputedRef(((ScoreAttribute) attr).asPipe())); + + if (expression instanceof ScalarFunction) { + return resolvedTreeComputingRef((ScalarFunction) expression, attr); } throw new SqlIllegalArgumentException("Unknown output attribute {}", attr); } - public QueryContainer addColumn(FieldExtraction ref, Attribute attr) { - ExpressionId id = attr instanceof AggregateFunctionAttribute ? ((AggregateFunctionAttribute) attr).innerId() : attr.id(); + public QueryContainer addColumn(FieldExtraction ref, String id) { return new QueryContainer(query, aggs, combine(fields, new Tuple<>(ref, id)), aliases, pseudoFunctions, scalarFunctions, sort, limit, trackHits, includeFrozen, minPageSize); @@ -487,8 +497,8 @@ public QueryContainer addGroups(Collection values) { return with(aggs.addGroups(values)); } - public GroupByKey findGroupForAgg(Attribute attr) { - return aggs.findGroupForAgg(attr); + public GroupByKey findGroupForAgg(String aggId) { + return aggs.findGroupForAgg(aggId); } public QueryContainer updateGroup(GroupByKey group) { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/tree/Node.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/tree/Node.java index 2e40244a41589..0d686fc5cf16a 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/tree/Node.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/tree/Node.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.BitSet; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -378,7 +379,10 @@ public String propertiesToString(boolean skipIfChild) { if (needsComma) { sb.append(","); } - String stringValue = Objects.toString(prop); + + String stringValue = toString(prop); + + //: Objects.toString(prop); if (maxWidth + stringValue.length() > TO_STRING_MAX_WIDTH) { int cutoff = Math.max(0, TO_STRING_MAX_WIDTH - maxWidth); sb.append(stringValue.substring(0, cutoff)); @@ -395,4 +399,28 @@ public String propertiesToString(boolean skipIfChild) { return sb.toString(); } + + private String toString(Object obj) { + StringBuilder sb = new StringBuilder(); + toString(sb, obj); + return sb.toString(); + } + + private void toString(StringBuilder sb, Object obj) { + if (obj instanceof Iterable) { + sb.append("["); + for (Iterator it = ((Iterable) obj).iterator(); it.hasNext();) { + Object o = it.next(); + toString(sb, o); + if (it.hasNext() == true) { + sb.append(", "); + } + } + sb.append("]"); + } else if (obj instanceof Node) { + sb.append(((Node) obj).nodeString()); + } else { + sb.append(Objects.toString(obj)); + } + } } \ No newline at end of file diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java index 04e58d9fc874c..08c2548a41ba3 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java @@ -437,7 +437,7 @@ public void testGroupByOrderByScalarOverNonGrouped() { } public void testGroupByOrderByFieldFromGroupByFunction() { - assertEquals("1:54: Cannot use non-grouped column [int], expected [ABS(int)]", + assertEquals("1:54: Cannot order by non-grouped column [int], expected [ABS(int)]", error("SELECT ABS(int) FROM test GROUP BY ABS(int) ORDER BY int")); } @@ -613,9 +613,9 @@ public void testInvalidTypeForBooleanFunction_WithOneArg() { } public void testInvalidTypeForStringFunction_WithTwoArgs() { - assertEquals("1:8: first argument of [CONCAT] must be [string], found value [1] type [integer]", + assertEquals("1:8: first argument of [CONCAT(1, 'bar')] must be [string], found value [1] type [integer]", error("SELECT CONCAT(1, 'bar')")); - assertEquals("1:8: second argument of [CONCAT] must be [string], found value [2] type [integer]", + assertEquals("1:8: second argument of [CONCAT('foo', 2)] must be [string], found value [2] type [integer]", error("SELECT CONCAT('foo', 2)")); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/SourceGeneratorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/SourceGeneratorTests.java index fce24758a3b4d..7efbea74241e0 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/SourceGeneratorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/SourceGeneratorTests.java @@ -14,7 +14,11 @@ import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.expression.Attribute; +import org.elasticsearch.xpack.sql.expression.AttributeMap; +import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.ReferenceAttribute; import org.elasticsearch.xpack.sql.expression.function.Score; import org.elasticsearch.xpack.sql.querydsl.agg.AvgAgg; import org.elasticsearch.xpack.sql.querydsl.agg.GroupByValue; @@ -27,6 +31,9 @@ import org.elasticsearch.xpack.sql.tree.Source; import org.elasticsearch.xpack.sql.type.KeywordEsField; +import java.util.LinkedHashMap; +import java.util.Map; + import static java.util.Collections.singletonList; import static org.elasticsearch.index.query.QueryBuilders.boolQuery; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; @@ -79,7 +86,11 @@ public void testSortNoneSpecified() { } public void testSelectScoreForcesTrackingScore() { - QueryContainer container = new QueryContainer().addColumn(new Score(Source.EMPTY).toAttribute()); + Score score = new Score(Source.EMPTY); + ReferenceAttribute attr = new ReferenceAttribute(score.source(), "score", score.dataType()); + Map alias = new LinkedHashMap<>(); + alias.put(attr, score); + QueryContainer container = new QueryContainer().withAliases(new AttributeMap<>(alias)).addColumn(attr); SearchSourceBuilder sourceBuilder = SourceGenerator.sourceBuilder(container, null, randomIntBetween(1, 10)); assertTrue(sourceBuilder.trackScores()); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/AttributeMapTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/AttributeMapTests.java index f2a6045124e4b..ee977687d902d 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/AttributeMapTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/AttributeMapTests.java @@ -55,7 +55,7 @@ public void testMapConstructor() { Attribute one = m.keySet().iterator().next(); assertThat(m.containsKey(one), is(true)); - assertThat(m.containsKey(a("one")), is(true)); + assertThat(m.containsKey(a("one")), is(false)); assertThat(m.containsValue("one"), is(true)); assertThat(m.containsValue("on"), is(false)); assertThat(m.attributeNames(), contains("one", "two", "three")); @@ -74,7 +74,7 @@ public void testSingleItemConstructor() { assertThat(m.isEmpty(), is(false)); assertThat(m.containsKey(one), is(true)); - assertThat(m.containsKey(a("one")), is(true)); + assertThat(m.containsKey(a("one")), is(false)); assertThat(m.containsValue("one"), is(true)); assertThat(m.containsValue("on"), is(false)); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/ExpressionIdTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/ExpressionIdTests.java index 3efa228f7ccea..dfbe34104342b 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/ExpressionIdTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/ExpressionIdTests.java @@ -10,11 +10,11 @@ public class ExpressionIdTests extends ESTestCase { /** - * Each {@link ExpressionId} should be unique. Technically + * Each {@link NameId} should be unique. Technically * you can roll the {@link AtomicLong} that backs them but * that is not going to happen within a single query. */ public void testUnique() { - assertNotEquals(new ExpressionId(), new ExpressionId()); + assertNotEquals(new NameId(), new NameId()); } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/LiteralTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/LiteralTests.java index 2d36cb1e1e56c..cd5e736c47c40 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/LiteralTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/LiteralTests.java @@ -61,7 +61,7 @@ protected Literal randomInstance() { @Override protected Literal copy(Literal instance) { - return new Literal(instance.source(), instance.name(), instance.value(), instance.dataType()); + return new Literal(instance.source(), instance.value(), instance.dataType()); } @Override diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/UnresolvedAttributeTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/UnresolvedAttributeTests.java index 4deca1d1f6362..a40e7661dc03d 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/UnresolvedAttributeTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/UnresolvedAttributeTests.java @@ -18,7 +18,7 @@ public static UnresolvedAttribute randomUnresolvedAttribute() { Source source = SourceTests.randomSource(); String name = randomAlphaOfLength(5); String qualifier = randomQualifier(); - ExpressionId id = randomBoolean() ? null : new ExpressionId(); + NameId id = randomBoolean() ? null : new NameId(); String unresolvedMessage = randomUnresolvedMessage(); Object resolutionMetadata = new Object(); return new UnresolvedAttribute(source, name, qualifier, id, unresolvedMessage, resolutionMetadata); @@ -82,7 +82,7 @@ public void testTransform() { a.unresolvedMessage(), a.resolutionMetadata()), a.transformPropertiesOnly(v -> Objects.equals(v, a.qualifier()) ? newQualifier : v, Object.class)); - ExpressionId newId = new ExpressionId(); + NameId newId = new NameId(); assertEquals(new UnresolvedAttribute(a.source(), a.name(), a.qualifier(), newId, a.unresolvedMessage(), a.resolutionMetadata()), a.transformPropertiesOnly(v -> Objects.equals(v, a.id()) ? newId : v, Object.class)); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/DatabaseFunctionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/DatabaseFunctionTests.java index 0156d8fdfb59a..8ad04d83c4c45 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/DatabaseFunctionTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/DatabaseFunctionTests.java @@ -11,6 +11,8 @@ import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; import org.elasticsearch.xpack.sql.analysis.index.EsIndex; import org.elasticsearch.xpack.sql.analysis.index.IndexResolution; +import org.elasticsearch.xpack.sql.expression.Alias; +import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.plan.logical.Project; @@ -38,7 +40,9 @@ null, clusterName, randomBoolean(), randomBoolean()), ); Project result = (Project) analyzer.analyze(parser.createStatement("SELECT DATABASE()"), true); - assertTrue(result.projections().get(0) instanceof Database); - assertEquals(clusterName, ((Database) result.projections().get(0)).fold()); + NamedExpression ne = result.projections().get(0); + assertTrue(ne instanceof Alias); + assertTrue(((Alias) ne).child() instanceof Database); + assertEquals(clusterName, ((Database) ((Alias) ne).child()).fold()); } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/UserFunctionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/UserFunctionTests.java index f8b3ed1976450..a6e8d83a336f7 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/UserFunctionTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/UserFunctionTests.java @@ -11,6 +11,8 @@ import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; import org.elasticsearch.xpack.sql.analysis.index.EsIndex; import org.elasticsearch.xpack.sql.analysis.index.IndexResolution; +import org.elasticsearch.xpack.sql.expression.Alias; +import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.plan.logical.Project; @@ -28,9 +30,9 @@ public void testNoUsernameFunctionOutput() { EsIndex test = new EsIndex("test", TypesTests.loadMapping("mapping-basic.json", true)); Analyzer analyzer = new Analyzer( new Configuration(DateUtils.UTC, Protocol.FETCH_SIZE, Protocol.REQUEST_TIMEOUT, - Protocol.PAGE_TIMEOUT, null, - randomFrom(Mode.values()), randomAlphaOfLength(10), - null, randomAlphaOfLengthBetween(1, 15), + Protocol.PAGE_TIMEOUT, null, + randomFrom(Mode.values()), randomAlphaOfLength(10), + null, randomAlphaOfLengthBetween(1, 15), randomBoolean(), randomBoolean()), new FunctionRegistry(), IndexResolution.valid(test), @@ -38,7 +40,9 @@ null, randomAlphaOfLengthBetween(1, 15), ); Project result = (Project) analyzer.analyze(parser.createStatement("SELECT USER()"), true); - assertTrue(result.projections().get(0) instanceof User); - assertNull(((User) result.projections().get(0)).fold()); + NamedExpression ne = result.projections().get(0); + assertTrue(ne instanceof Alias); + assertTrue(((Alias) ne).child() instanceof User); + assertNull(((User) ((Alias) ne).child()).fold()); } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/CaseTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/CaseTests.java index 00004598f5c97..899da8049b915 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/CaseTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/CaseTests.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.sql.expression.predicate.conditional; import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.Expression.TypeResolution; import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils; import org.elasticsearch.xpack.sql.expression.predicate.operator.comparison.Equals; @@ -21,7 +22,6 @@ import java.util.List; import java.util.Objects; -import static org.elasticsearch.xpack.sql.expression.Expression.TypeResolution; import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomIntLiteral; import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomStringLiteral; import static org.elasticsearch.xpack.sql.tree.Source.EMPTY; @@ -69,10 +69,6 @@ public void testTransform() { Source newSource = randomValueOtherThan(c.source(), SourceTests::randomSource); assertEquals(new Case(c.source(), c.children()), c.transformPropertiesOnly(p -> Objects.equals(p, c.source()) ? newSource: p, Object.class)); - - String newName = randomValueOtherThan(c.name(), () -> randomAlphaOfLength(5)); - assertEquals(new Case(c.source(), c.children()), - c.transformPropertiesOnly(p -> Objects.equals(p, c.name()) ? newName : p, Object.class)); } @Override diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/IifTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/IifTests.java index a07663b188d25..6b468fcb8fbf2 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/IifTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/IifTests.java @@ -62,10 +62,6 @@ public void testTransform() { Source newSource = randomValueOtherThan(iif.source(), SourceTests::randomSource); assertEquals(new Iif(iif.source(), iif.conditions().get(0).condition(), iif.conditions().get(0).result(), iif.elseResult()), iif.transformPropertiesOnly(p -> Objects.equals(p, iif.source()) ? newSource: p, Object.class)); - - String newName = randomValueOtherThan(iif.name(), () -> randomAlphaOfLength(5)); - assertEquals(new Iif(iif.source(), iif.conditions().get(0).condition(), iif.conditions().get(0).result(), iif.elseResult()), - iif.transformPropertiesOnly(p -> Objects.equals(p, iif.name()) ? newName : p, Object.class)); } @Override diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java index 8efb687428945..c81b376c0a534 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java @@ -22,7 +22,6 @@ import org.elasticsearch.xpack.sql.expression.function.Function; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; import org.elasticsearch.xpack.sql.expression.function.aggregate.Avg; -import org.elasticsearch.xpack.sql.expression.function.aggregate.Count; import org.elasticsearch.xpack.sql.expression.function.aggregate.ExtendedStats; import org.elasticsearch.xpack.sql.expression.function.aggregate.First; import org.elasticsearch.xpack.sql.expression.function.aggregate.InnerAggregate; @@ -101,11 +100,11 @@ import org.elasticsearch.xpack.sql.optimizer.Optimizer.ConstantFolding; import org.elasticsearch.xpack.sql.optimizer.Optimizer.FoldNull; import org.elasticsearch.xpack.sql.optimizer.Optimizer.PropagateEquals; -import org.elasticsearch.xpack.sql.optimizer.Optimizer.PruneDuplicateFunctions; import org.elasticsearch.xpack.sql.optimizer.Optimizer.ReplaceAggsWithExtendedStats; import org.elasticsearch.xpack.sql.optimizer.Optimizer.ReplaceAggsWithStats; import org.elasticsearch.xpack.sql.optimizer.Optimizer.ReplaceFoldableAttributes; import org.elasticsearch.xpack.sql.optimizer.Optimizer.ReplaceMinMaxWithTopHits; +import org.elasticsearch.xpack.sql.optimizer.Optimizer.ReplaceReferenceAttributeWithSource; import org.elasticsearch.xpack.sql.optimizer.Optimizer.RewritePivot; import org.elasticsearch.xpack.sql.optimizer.Optimizer.SimplifyCase; import org.elasticsearch.xpack.sql.optimizer.Optimizer.SimplifyConditional; @@ -144,7 +143,7 @@ import static org.elasticsearch.xpack.sql.tree.Source.EMPTY; import static org.elasticsearch.xpack.sql.util.DateUtils.UTC; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.is; public class OptimizerTests extends ESTestCase { @@ -210,6 +209,10 @@ private static Literal L(Object value) { return of(EMPTY, value); } + private static Alias a(String name, Expression e) { + return new Alias(e.source(), name, e); + } + private static FieldAttribute getFieldAttribute() { return getFieldAttribute("a"); } @@ -225,20 +228,6 @@ public void testPruneSubqueryAliases() { assertEquals(result, s); } - public void testDuplicateFunctions() { - AggregateFunction f1 = new Count(EMPTY, TRUE, false); - AggregateFunction f2 = new Count(EMPTY, TRUE, false); - - assertTrue(f1.functionEquals(f2)); - - Project p = new Project(EMPTY, FROM(), Arrays.asList(f1, f2)); - LogicalPlan result = new PruneDuplicateFunctions().apply(p); - assertTrue(result instanceof Project); - List projections = ((Project) result).projections(); - assertEquals(2, projections.size()); - assertEquals(projections.get(0), projections.get(1)); - } - public void testCombineProjections() { // a Alias a = new Alias(EMPTY, "a", FIVE); @@ -338,17 +327,17 @@ public void testConstantFoldingBinaryLogic() { } public void testConstantFoldingBinaryLogic_WithNullHandling() { - assertEquals(NULL, new ConstantFolding().rule(new And(EMPTY, NULL, TRUE)).canonical()); - assertEquals(NULL, new ConstantFolding().rule(new And(EMPTY, TRUE, NULL)).canonical()); + assertEquals(Nullability.TRUE, new ConstantFolding().rule(new And(EMPTY, NULL, TRUE)).canonical().nullable()); + assertEquals(Nullability.TRUE, new ConstantFolding().rule(new And(EMPTY, TRUE, NULL)).canonical().nullable()); assertEquals(FALSE, new ConstantFolding().rule(new And(EMPTY, NULL, FALSE)).canonical()); assertEquals(FALSE, new ConstantFolding().rule(new And(EMPTY, FALSE, NULL)).canonical()); - assertEquals(NULL, new ConstantFolding().rule(new And(EMPTY, NULL, NULL)).canonical()); + assertEquals(Nullability.TRUE, new ConstantFolding().rule(new And(EMPTY, NULL, NULL)).canonical().nullable()); assertEquals(TRUE, new ConstantFolding().rule(new Or(EMPTY, NULL, TRUE)).canonical()); assertEquals(TRUE, new ConstantFolding().rule(new Or(EMPTY, TRUE, NULL)).canonical()); - assertEquals(NULL, new ConstantFolding().rule(new Or(EMPTY, NULL, FALSE)).canonical()); - assertEquals(NULL, new ConstantFolding().rule(new Or(EMPTY, FALSE, NULL)).canonical()); - assertEquals(NULL, new ConstantFolding().rule(new Or(EMPTY, NULL, NULL)).canonical()); + assertEquals(Nullability.TRUE, new ConstantFolding().rule(new Or(EMPTY, NULL, FALSE)).canonical().nullable()); + assertEquals(Nullability.TRUE, new ConstantFolding().rule(new Or(EMPTY, FALSE, NULL)).canonical().nullable()); + assertEquals(Nullability.TRUE, new ConstantFolding().rule(new Or(EMPTY, NULL, NULL)).canonical().nullable()); } public void testConstantFoldingRange() { @@ -393,13 +382,15 @@ public void testConstantFoldingIn() { } public void testConstantFoldingIn_LeftValueNotFoldable() { - Project p = new Project(EMPTY, FROM(), Collections.singletonList( - new In(EMPTY, getFieldAttribute(), - Arrays.asList(ONE, TWO, ONE, THREE, new Sub(EMPTY, THREE, ONE), ONE, FOUR, new Abs(EMPTY, new Sub(EMPTY, TWO, FIVE)))))); + In in = new In(EMPTY, getFieldAttribute(), + Arrays.asList(ONE, TWO, ONE, THREE, new Sub(EMPTY, THREE, ONE), ONE, FOUR, new Abs(EMPTY, new Sub(EMPTY, TWO, FIVE)))); + Alias as = new Alias(in.source(), in.sourceText(), in); + Project p = new Project(EMPTY, FROM(), Collections.singletonList(as)); p = (Project) new ConstantFolding().apply(p); assertEquals(1, p.projections().size()); - In in = (In) p.projections().get(0); - assertThat(Foldables.valuesOf(in.list(), DataType.INTEGER), contains(1 ,2 ,3 ,4)); + Alias a = (Alias) p.projections().get(0); + In i = (In) a.child(); + assertThat(Foldables.valuesOf(i.list(), DataType.INTEGER), contains(1 ,2 ,3 ,4)); } public void testConstantFoldingIn_RightValueIsNull() { @@ -672,47 +663,12 @@ public void testSimplifyCaseConditionsFoldWhenFalse() { new IfConditional(EMPTY, new GreaterThan(EMPTY, getFieldAttribute(), ONE), Literal.of(EMPTY, "foo2")), Literal.of(EMPTY, "default"))); assertFalse(c.foldable()); - Expression e = new SimplifyCase().rule(c); assertEquals(Case.class, e.getClass()); c = (Case) e; assertEquals(2, c.conditions().size()); - assertThat(c.conditions().get(0).condition().toString(), startsWith("Equals[a{f}#")); - assertThat(c.conditions().get(1).condition().toString(), startsWith("GreaterThan[a{f}#")); - assertFalse(c.foldable()); - assertEquals(TypeResolution.TYPE_RESOLVED, c.typeResolved()); - } - - public void testSimplifyCaseConditionsFoldWhenTrue() { - // CASE WHEN a = 1 THEN 'foo1' - // WHEN 1 = 1 THEN 'bar1' - // WHEN 2 = 1 THEN 'bar2' - // WHEN a > 1 THEN 'foo2' - // ELSE 'default' - // END - // - // ==> - // - // CASE WHEN a = 1 THEN 'foo1' - // WHEN 1 = 1 THEN 'bar1' - // ELSE 'default' - // END - - Case c = new Case(EMPTY, Arrays.asList( - new IfConditional(EMPTY, new Equals(EMPTY, getFieldAttribute(), ONE), Literal.of(EMPTY, "foo1")), - new IfConditional(EMPTY, new Equals(EMPTY, ONE, ONE), Literal.of(EMPTY, "bar1")), - new IfConditional(EMPTY, new Equals(EMPTY, TWO, ONE), Literal.of(EMPTY, "bar2")), - new IfConditional(EMPTY, new GreaterThan(EMPTY, getFieldAttribute(), ONE), Literal.of(EMPTY, "foo2")), - Literal.of(EMPTY, "default"))); - assertFalse(c.foldable()); - - SimplifyCase rule = new SimplifyCase(); - Expression e = rule.rule(c); - assertEquals(Case.class, e.getClass()); - c = (Case) e; - assertEquals(2, c.conditions().size()); - assertThat(c.conditions().get(0).condition().toString(), startsWith("Equals[a{f}#")); - assertThat(c.conditions().get(1).condition().toString(), startsWith("Equals[=1,=1]#")); + assertThat(c.conditions().get(0).condition().getClass(), is(Equals.class)); + assertThat(c.conditions().get(1).condition().getClass(), is(GreaterThan.class)); assertFalse(c.foldable()); assertEquals(TypeResolution.TYPE_RESOLVED, c.typeResolved()); } @@ -738,7 +694,7 @@ public void testSimplifyCaseConditionsFoldCompletely_FoldableElse() { assertEquals(Case.class, e.getClass()); c = (Case) e; assertEquals(1, c.conditions().size()); - assertThat(c.conditions().get(0).condition().toString(), startsWith("Equals[=1,=1]#")); + assertThat(c.conditions().get(0).condition().nodeString(), is("1[INTEGER] == 1[INTEGER]")); assertTrue(c.foldable()); assertEquals("foo2", c.fold()); assertEquals(TypeResolution.TYPE_RESOLVED, c.typeResolved()); @@ -822,7 +778,7 @@ public void testSimplifyIif_ConditionFalse_NonFoldableResult() { assertFalse(iif.foldable()); assertEquals("myField", Expressions.name(iif.elseResult())); } - + // // Logical simplifications // @@ -854,13 +810,11 @@ public void testNullEqualsWithNullLiteralBecomesIsNull() { assertEquals(IsNull.class, e.getClass()); IsNull isNull = (IsNull) e; assertEquals(source, isNull.source()); - assertEquals("IS_NULL(a)", isNull.name()); e = bcSimpl.rule(swapLiteralsToRight.rule(new NullEquals(source, NULL, fa))); assertEquals(IsNull.class, e.getClass()); isNull = (IsNull) e; assertEquals(source, isNull.source()); - assertEquals("IS_NULL(a)", isNull.name()); } public void testLiteralsOnTheRight() { @@ -1500,7 +1454,8 @@ public void testTranslateMinToFirst() { Min min1 = new Min(EMPTY, new FieldAttribute(EMPTY, "str", new EsField("str", DataType.KEYWORD, emptyMap(), true))); Min min2 = new Min(EMPTY, getFieldAttribute()); - OrderBy plan = new OrderBy(EMPTY, new Aggregate(EMPTY, FROM(), emptyList(), Arrays.asList(min1, min2)), + OrderBy plan = new OrderBy(EMPTY, new Aggregate(EMPTY, FROM(), emptyList(), + Arrays.asList(a("min1", min1), a("min2", min2))), Arrays.asList( new Order(EMPTY, min1, OrderDirection.ASC, Order.NullsPosition.LAST), new Order(EMPTY, min2, OrderDirection.ASC, Order.NullsPosition.LAST))); @@ -1515,16 +1470,17 @@ public void testTranslateMinToFirst() { assertTrue(((OrderBy) result).child() instanceof Aggregate); List aggregates = ((Aggregate) ((OrderBy) result).child()).aggregates(); assertEquals(2, aggregates.size()); - assertEquals(First.class, aggregates.get(0).getClass()); - assertSame(first, aggregates.get(0)); - assertEquals(min2, aggregates.get(1)); + assertEquals(Alias.class, aggregates.get(0).getClass()); + assertEquals(Alias.class, aggregates.get(1).getClass()); + assertSame(first, ((Alias) aggregates.get(0)).child()); + assertEquals(min2, ((Alias) aggregates.get(1)).child()); } public void testTranslateMaxToLast() { Max max1 = new Max(EMPTY, new FieldAttribute(EMPTY, "str", new EsField("str", DataType.KEYWORD, emptyMap(), true))); Max max2 = new Max(EMPTY, getFieldAttribute()); - OrderBy plan = new OrderBy(EMPTY, new Aggregate(EMPTY, FROM(), emptyList(), Arrays.asList(max1, max2)), + OrderBy plan = new OrderBy(EMPTY, new Aggregate(EMPTY, FROM(), emptyList(), Arrays.asList(a("max1", max1), a("max2", max2))), Arrays.asList( new Order(EMPTY, max1, OrderDirection.ASC, Order.NullsPosition.LAST), new Order(EMPTY, max2, OrderDirection.ASC, Order.NullsPosition.LAST))); @@ -1538,9 +1494,10 @@ public void testTranslateMaxToLast() { assertTrue(((OrderBy) result).child() instanceof Aggregate); List aggregates = ((Aggregate) ((OrderBy) result).child()).aggregates(); assertEquals(2, aggregates.size()); - assertEquals(Last.class, aggregates.get(0).getClass()); - assertSame(last, aggregates.get(0)); - assertEquals(max2, aggregates.get(1)); + assertEquals(Alias.class, aggregates.get(0).getClass()); + assertEquals(Alias.class, aggregates.get(1).getClass()); + assertSame(last, ((Alias) aggregates.get(0)).child()); + assertEquals(max2, ((Alias) aggregates.get(1)).child()); } public void testSortAggregateOnOrderByWithTwoFields() { @@ -1551,12 +1508,12 @@ public void testSortAggregateOnOrderByWithTwoFields() { Alias secondAlias = new Alias(EMPTY, "second_alias", secondField); Order firstOrderBy = new Order(EMPTY, firstField, OrderDirection.ASC, Order.NullsPosition.LAST); Order secondOrderBy = new Order(EMPTY, secondField, OrderDirection.ASC, Order.NullsPosition.LAST); - + OrderBy orderByPlan = new OrderBy(EMPTY, new Aggregate(EMPTY, FROM(), Arrays.asList(secondField, firstField), Arrays.asList(secondAlias, firstAlias)), Arrays.asList(firstOrderBy, secondOrderBy)); LogicalPlan result = new SortAggregateOnOrderBy().apply(orderByPlan); - + assertTrue(result instanceof OrderBy); List order = ((OrderBy) result).order(); assertEquals(2, order.size()); @@ -1564,7 +1521,7 @@ public void testSortAggregateOnOrderByWithTwoFields() { assertTrue(order.get(1).child() instanceof FieldAttribute); assertEquals("first_field", ((FieldAttribute) order.get(0).child()).name()); assertEquals("second_field", ((FieldAttribute) order.get(1).child()).name()); - + assertTrue(((OrderBy) result).child() instanceof Aggregate); Aggregate agg = (Aggregate) ((OrderBy) result).child(); List groupings = agg.groupings(); @@ -1583,12 +1540,12 @@ public void testSortAggregateOnOrderByOnlyAliases() { Alias secondAlias = new Alias(EMPTY, "second_alias", secondField); Order firstOrderBy = new Order(EMPTY, firstAlias, OrderDirection.ASC, Order.NullsPosition.LAST); Order secondOrderBy = new Order(EMPTY, secondAlias, OrderDirection.ASC, Order.NullsPosition.LAST); - + OrderBy orderByPlan = new OrderBy(EMPTY, new Aggregate(EMPTY, FROM(), Arrays.asList(secondAlias, firstAlias), Arrays.asList(secondAlias, firstAlias)), Arrays.asList(firstOrderBy, secondOrderBy)); LogicalPlan result = new SortAggregateOnOrderBy().apply(orderByPlan); - + assertTrue(result instanceof OrderBy); List order = ((OrderBy) result).order(); assertEquals(2, order.size()); @@ -1596,7 +1553,7 @@ public void testSortAggregateOnOrderByOnlyAliases() { assertTrue(order.get(1).child() instanceof Alias); assertEquals("first_alias", ((Alias) order.get(0).child()).name()); assertEquals("second_alias", ((Alias) order.get(1).child()).name()); - + assertTrue(((OrderBy) result).child() instanceof Aggregate); Aggregate agg = (Aggregate) ((OrderBy) result).child(); List groupings = agg.groupings(); @@ -1611,7 +1568,7 @@ public void testPivotRewrite() { FieldAttribute column = getFieldAttribute("pivot"); FieldAttribute number = getFieldAttribute("number"); List values = Arrays.asList(new Alias(EMPTY, "ONE", L(1)), new Alias(EMPTY, "TWO", L(2))); - List aggs = Arrays.asList(new Avg(EMPTY, number)); + List aggs = Arrays.asList(new Alias(EMPTY, "AVG", new Avg(EMPTY, number))); Pivot pivot = new Pivot(EMPTY, new EsRelation(EMPTY, new EsIndex("table", emptyMap()), false), column, values, aggs); LogicalPlan result = new RewritePivot().apply(pivot); @@ -1657,8 +1614,8 @@ public void testAggregatesPromoteToStats_WithFullTextPredicatesConditions() { } AggregateFunction firstAggregate = randomFrom(aggregates); AggregateFunction secondAggregate = randomValueOtherThan(firstAggregate, () -> randomFrom(aggregates)); - Aggregate aggregatePlan = new Aggregate(EMPTY, filter, Collections.singletonList(matchField), - Arrays.asList(firstAggregate, secondAggregate)); + Aggregate aggregatePlan = new Aggregate(EMPTY, filter, singletonList(matchField), + Arrays.asList(new Alias(EMPTY, "first", firstAggregate), new Alias(EMPTY, "second", secondAggregate))); LogicalPlan result; if (isSimpleStats) { result = new ReplaceAggsWithStats().apply(aggregatePlan); @@ -1669,11 +1626,17 @@ public void testAggregatesPromoteToStats_WithFullTextPredicatesConditions() { assertTrue(result instanceof Aggregate); Aggregate resultAgg = (Aggregate) result; assertEquals(2, resultAgg.aggregates().size()); - assertTrue(resultAgg.aggregates().get(0) instanceof InnerAggregate); - assertTrue(resultAgg.aggregates().get(1) instanceof InnerAggregate); - InnerAggregate resultFirstAgg = (InnerAggregate) resultAgg.aggregates().get(0); - InnerAggregate resultSecondAgg = (InnerAggregate) resultAgg.aggregates().get(1); + NamedExpression one = resultAgg.aggregates().get(0); + assertTrue(one instanceof Alias); + assertTrue(((Alias) one).child() instanceof InnerAggregate); + + NamedExpression two = resultAgg.aggregates().get(1); + assertTrue(two instanceof Alias); + assertTrue(((Alias) two).child() instanceof InnerAggregate); + + InnerAggregate resultFirstAgg = (InnerAggregate) ((Alias) one).child(); + InnerAggregate resultSecondAgg = (InnerAggregate) ((Alias) two).child(); assertEquals(resultFirstAgg.inner(), firstAggregate); assertEquals(resultSecondAgg.inner(), secondAggregate); if (isSimpleStats) { @@ -1691,4 +1654,34 @@ public void testAggregatesPromoteToStats_WithFullTextPredicatesConditions() { assertTrue(resultAgg.child() instanceof Filter); assertEquals(resultAgg.child(), filter); } -} + + public void testReplaceAttributesWithTarget() { + FieldAttribute a = getFieldAttribute("a"); + FieldAttribute b = getFieldAttribute("b"); + + Alias aAlias = new Alias(EMPTY, "aAlias", a); + Alias bAlias = new Alias(EMPTY, "bAlias", b); + + Project p = new Project(EMPTY, FROM(), Arrays.asList(aAlias, bAlias)); + Filter f = new Filter(EMPTY, p, + new And(EMPTY, new GreaterThan(EMPTY, aAlias.toAttribute(), L(1)), new GreaterThan(EMPTY, bAlias.toAttribute(), L(2)))); + + ReplaceReferenceAttributeWithSource rule = new ReplaceReferenceAttributeWithSource(); + Expression condition = f.condition(); + assertTrue(condition instanceof And); + And and = (And) condition; + assertTrue(and.left() instanceof GreaterThan); + GreaterThan gt = (GreaterThan) and.left(); + assertEquals(aAlias.toAttribute(), gt.left()); + + LogicalPlan plan = rule.apply(f); + + Filter filter = (Filter) plan; + condition = filter.condition(); + assertTrue(condition instanceof And); + and = (And) condition; + assertTrue(and.left() instanceof GreaterThan); + gt = (GreaterThan) and.left(); + assertEquals(a, gt.left()); + } +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/ExpressionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/ExpressionTests.java index c9fb153f57e02..8d25901650b65 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/ExpressionTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/ExpressionTests.java @@ -493,8 +493,8 @@ public void testCaseWithoutOperand() { assertEquals(3, c.conditions().size()); IfConditional ifc = c.conditions().get(0); assertEquals("WHEN a = 1 THEN 'one'", ifc.sourceText()); - assertThat(ifc.condition().toString(), startsWith("Equals[?a,1]#")); - assertEquals("'one'=one", ifc.result().toString()); + assertThat(ifc.condition().toString(), startsWith("a = 1")); + assertEquals("one", ifc.result().toString()); assertEquals(Literal.NULL, c.elseResult()); expr = parser.createExpression( @@ -508,7 +508,7 @@ public void testCaseWithoutOperand() { assertEquals(2, c.conditions().size()); ifc = c.conditions().get(0); assertEquals("WHEN a = 1 THEN 'one'", ifc.sourceText()); - assertEquals("'many'=many", c.elseResult().toString()); + assertEquals("many", c.elseResult().toString()); } public void testCaseWithOperand() { @@ -523,8 +523,8 @@ public void testCaseWithOperand() { assertEquals(3, c.conditions().size()); IfConditional ifc = c.conditions().get(0); assertEquals("WHEN 1 THEN 'one'", ifc.sourceText()); - assertThat(ifc.condition().toString(), startsWith("Equals[?a,1]#")); - assertEquals("'one'=one", ifc.result().toString()); + assertThat(ifc.condition().toString(), startsWith("WHEN 1 THEN 'one'")); + assertEquals("one", ifc.result().toString()); assertEquals(Literal.NULL, c.elseResult()); expr = parser.createExpression( @@ -537,6 +537,6 @@ public void testCaseWithOperand() { assertEquals(2, c.conditions().size()); ifc = c.conditions().get(0); assertEquals("WHEN 1 THEN 'one'", ifc.sourceText()); - assertEquals("'many'=many", c.elseResult().toString()); + assertEquals("many", c.elseResult().toString()); } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java index ca31e32b2edc3..1dc9567016ec5 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java @@ -12,14 +12,12 @@ import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.expression.Order; +import org.elasticsearch.xpack.sql.expression.UnresolvedAlias; import org.elasticsearch.xpack.sql.expression.UnresolvedAttribute; -import org.elasticsearch.xpack.sql.expression.UnresolvedStar; import org.elasticsearch.xpack.sql.expression.function.UnresolvedFunction; -import org.elasticsearch.xpack.sql.expression.function.scalar.Cast; import org.elasticsearch.xpack.sql.expression.predicate.fulltext.MatchQueryPredicate; import org.elasticsearch.xpack.sql.expression.predicate.fulltext.MultiMatchQueryPredicate; import org.elasticsearch.xpack.sql.expression.predicate.fulltext.StringQueryPredicate; -import org.elasticsearch.xpack.sql.expression.predicate.operator.arithmetic.Add; import org.elasticsearch.xpack.sql.plan.logical.Filter; import org.elasticsearch.xpack.sql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.sql.plan.logical.OrderBy; @@ -40,7 +38,7 @@ public class SqlParserTests extends ESTestCase { public void testSelectStar() { - singleProjection(project(parseStatement("SELECT * FROM foo")), UnresolvedStar.class); + singleProjection(project(parseStatement("SELECT * FROM foo")), UnresolvedAlias.class); } private T singleProjection(Project project, Class type) { @@ -69,42 +67,44 @@ public void testEscapeSingleAndDoubleQuotes() { } public void testSelectField() { - UnresolvedAttribute a = singleProjection(project(parseStatement("SELECT bar FROM foo")), UnresolvedAttribute.class); - assertEquals("bar", a.name()); + UnresolvedAlias a = singleProjection(project(parseStatement("SELECT bar FROM foo")), UnresolvedAlias.class); + assertEquals("bar", a.sourceText()); } public void testSelectScore() { - UnresolvedFunction f = singleProjection(project(parseStatement("SELECT SCORE() FROM foo")), UnresolvedFunction.class); + UnresolvedAlias f = singleProjection(project(parseStatement("SELECT SCORE() FROM foo")), UnresolvedAlias.class); assertEquals("SCORE()", f.sourceText()); } public void testSelectCast() { - Cast f = singleProjection(project(parseStatement("SELECT CAST(POWER(languages, 2) AS DOUBLE) FROM foo")), Cast.class); + UnresolvedAlias f = singleProjection(project(parseStatement("SELECT CAST(POWER(languages, 2) AS DOUBLE) FROM foo")), + UnresolvedAlias.class); assertEquals("CAST(POWER(languages, 2) AS DOUBLE)", f.sourceText()); } public void testSelectCastOperator() { - Cast f = singleProjection(project(parseStatement("SELECT POWER(languages, 2)::DOUBLE FROM foo")), Cast.class); + UnresolvedAlias f = singleProjection(project(parseStatement("SELECT POWER(languages, 2)::DOUBLE FROM foo")), UnresolvedAlias.class); assertEquals("POWER(languages, 2)::DOUBLE", f.sourceText()); } public void testSelectCastWithSQLOperator() { - Cast f = singleProjection(project(parseStatement("SELECT CONVERT(POWER(languages, 2), SQL_DOUBLE) FROM foo")), Cast.class); + UnresolvedAlias f = singleProjection(project(parseStatement("SELECT CONVERT(POWER(languages, 2), SQL_DOUBLE) FROM foo")), + UnresolvedAlias.class); assertEquals("CONVERT(POWER(languages, 2), SQL_DOUBLE)", f.sourceText()); } public void testSelectCastToEsType() { - Cast f = singleProjection(project(parseStatement("SELECT CAST('0.' AS SCALED_FLOAT)")), Cast.class); + UnresolvedAlias f = singleProjection(project(parseStatement("SELECT CAST('0.' AS SCALED_FLOAT)")), UnresolvedAlias.class); assertEquals("CAST('0.' AS SCALED_FLOAT)", f.sourceText()); } public void testSelectAddWithParanthesis() { - Add f = singleProjection(project(parseStatement("SELECT (1 + 2)")), Add.class); - assertEquals("1 + 2", f.sourceText()); + UnresolvedAlias f = singleProjection(project(parseStatement("SELECT (1 + 2)")), UnresolvedAlias.class); + assertEquals("(1 + 2)", f.sourceText()); } public void testSelectRightFunction() { - UnresolvedFunction f = singleProjection(project(parseStatement("SELECT RIGHT()")), UnresolvedFunction.class); + UnresolvedAlias f = singleProjection(project(parseStatement("SELECT RIGHT()")), UnresolvedAlias.class); assertEquals("RIGHT()", f.sourceText()); } @@ -124,8 +124,8 @@ public void testsSelectNonReservedKeywords() { for (int i = 0; i < project.projections().size(); i++) { NamedExpression ne = project.projections().get(i); - assertEquals(UnresolvedAttribute.class, ne.getClass()); - assertEquals(reserved[i], ne.name()); + assertEquals(UnresolvedAlias.class, ne.getClass()); + assertEquals(reserved[i], ne.sourceText()); } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryFolderTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryFolderTests.java index 11f6cc949de44..18afb92b27361 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryFolderTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryFolderTests.java @@ -12,8 +12,8 @@ import org.elasticsearch.xpack.sql.analysis.index.EsIndex; import org.elasticsearch.xpack.sql.analysis.index.IndexResolution; import org.elasticsearch.xpack.sql.expression.Expressions; +import org.elasticsearch.xpack.sql.expression.ReferenceAttribute; import org.elasticsearch.xpack.sql.expression.function.FunctionRegistry; -import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; import org.elasticsearch.xpack.sql.optimizer.Optimizer; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.plan.physical.EsQueryExec; @@ -70,7 +70,7 @@ public void testFoldingToLocalExecWithProject() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingToLocalExecWithProjectAndLimit() { @@ -80,7 +80,7 @@ public void testFoldingToLocalExecWithProjectAndLimit() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingToLocalExecWithProjectAndOrderBy() { @@ -90,7 +90,7 @@ public void testFoldingToLocalExecWithProjectAndOrderBy() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingToLocalExecWithProjectAndOrderByAndLimit() { @@ -100,7 +100,7 @@ public void testFoldingToLocalExecWithProjectAndOrderByAndLimit() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testLocalExecWithPrunedFilterWithFunction() { @@ -110,7 +110,7 @@ public void testLocalExecWithPrunedFilterWithFunction() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("E(){c}#")); + assertThat(ee.output().get(0).toString(), startsWith("E(){r}#")); } public void testLocalExecWithPrunedFilterWithFunctionAndAggregation() { @@ -120,7 +120,7 @@ public void testLocalExecWithPrunedFilterWithFunctionAndAggregation() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("E(){c}#")); + assertThat(ee.output().get(0).toString(), startsWith("E(){r}#")); } public void testFoldingToLocalExecWithAggregationAndLimit() { @@ -130,7 +130,7 @@ public void testFoldingToLocalExecWithAggregationAndLimit() { assertEquals(SingletonExecutable.class, le.executable().getClass()); SingletonExecutable ee = (SingletonExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("'foo'{c}#")); + assertThat(ee.output().get(0).toString(), startsWith("'foo'{r}#")); } public void testFoldingToLocalExecWithAggregationAndOrderBy() { @@ -140,7 +140,7 @@ public void testFoldingToLocalExecWithAggregationAndOrderBy() { assertEquals(SingletonExecutable.class, le.executable().getClass()); SingletonExecutable ee = (SingletonExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("'foo'{c}#")); + assertThat(ee.output().get(0).toString(), startsWith("'foo'{r}#")); } public void testFoldingToLocalExecWithAggregationAndOrderByAndLimit() { @@ -150,7 +150,7 @@ public void testFoldingToLocalExecWithAggregationAndOrderByAndLimit() { assertEquals(SingletonExecutable.class, le.executable().getClass()); SingletonExecutable ee = (SingletonExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("'foo'{c}#")); + assertThat(ee.output().get(0).toString(), startsWith("'foo'{r}#")); } public void testLocalExecWithoutFromClause() { @@ -160,9 +160,9 @@ public void testLocalExecWithoutFromClause() { assertEquals(SingletonExecutable.class, le.executable().getClass()); SingletonExecutable ee = (SingletonExecutable) le.executable(); assertEquals(3, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("E(){c}#")); - assertThat(ee.output().get(1).toString(), startsWith("'foo'{c}#")); - assertThat(ee.output().get(2).toString(), startsWith("abs(10){c}#")); + assertThat(ee.output().get(0).toString(), startsWith("E(){r}#")); + assertThat(ee.output().get(1).toString(), startsWith("'foo'{r}#")); + assertThat(ee.output().get(2).toString(), startsWith("abs(10){r}#")); } public void testLocalExecWithoutFromClauseWithPrunedFilter() { @@ -172,7 +172,7 @@ public void testLocalExecWithoutFromClauseWithPrunedFilter() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("E(){c}#")); + assertThat(ee.output().get(0).toString(), startsWith("E(){r}#")); } public void testFoldingOfIsNull() { @@ -180,7 +180,7 @@ public void testFoldingOfIsNull() { assertEquals(LocalExec.class, p.getClass()); LocalExec ee = (LocalExec) p; assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingToLocalExecBooleanAndNull_WhereClause() { @@ -190,7 +190,7 @@ public void testFoldingToLocalExecBooleanAndNull_WhereClause() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingToLocalExecBooleanAndNull_HavingClause() { @@ -200,8 +200,8 @@ public void testFoldingToLocalExecBooleanAndNull_HavingClause() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); - assertThat(ee.output().get(1).toString(), startsWith("max(int){a->")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); + assertThat(ee.output().get(1).toString(), startsWith("max(int){r}")); } public void testFoldingBooleanOrNull_WhereClause() { @@ -211,7 +211,7 @@ public void testFoldingBooleanOrNull_WhereClause() { assertEquals("{\"range\":{\"int\":{\"from\":10,\"to\":null,\"include_lower\":false,\"include_upper\":false,\"boost\":1.0}}}", ee.queryContainer().query().asBuilder().toString().replaceAll("\\s+", "")); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingBooleanOrNull_HavingClause() { @@ -222,8 +222,8 @@ public void testFoldingBooleanOrNull_HavingClause() { "\"script\":{\"source\":\"InternalSqlScriptUtils.nullSafeFilter(InternalSqlScriptUtils.gt(params.a0,params.v0))\"," + "\"lang\":\"painless\",\"params\":{\"v0\":10}},")); assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); - assertThat(ee.output().get(1).toString(), startsWith("max(int){a->")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); + assertThat(ee.output().get(1).toString(), startsWith("max(int){r}")); } public void testFoldingOfIsNotNull() { @@ -231,7 +231,7 @@ public void testFoldingOfIsNotNull() { assertEquals(EsQueryExec.class, p.getClass()); EsQueryExec ee = (EsQueryExec) p; assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingToLocalExecWithNullFilter() { @@ -241,7 +241,7 @@ public void testFoldingToLocalExecWithNullFilter() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingToLocalExecWithProject_FoldableIn() { @@ -251,7 +251,7 @@ public void testFoldingToLocalExecWithProject_FoldableIn() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingToLocalExecWithProject_WithOrderAndLimit() { @@ -261,7 +261,7 @@ public void testFoldingToLocalExecWithProject_WithOrderAndLimit() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingToLocalExecWithProjectWithGroupBy_WithOrderAndLimit() { @@ -271,8 +271,8 @@ public void testFoldingToLocalExecWithProjectWithGroupBy_WithOrderAndLimit() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); - assertThat(ee.output().get(1).toString(), startsWith("max(int){a->")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); + assertThat(ee.output().get(1).toString(), startsWith("max(int){r}")); } public void testFoldingToLocalExecWithProjectWithGroupBy_WithHaving_WithOrderAndLimit() { @@ -282,8 +282,8 @@ public void testFoldingToLocalExecWithProjectWithGroupBy_WithHaving_WithOrderAnd assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); - assertThat(ee.output().get(1).toString(), startsWith("max(int){a->")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); + assertThat(ee.output().get(1).toString(), startsWith("max(int){r}")); } public void testGroupKeyTypes_Boolean() { @@ -296,8 +296,8 @@ public void testGroupKeyTypes_Boolean() { "\"lang\":\"painless\",\"params\":{\"v0\":\"int\",\"v1\":10}},\"missing_bucket\":true," + "\"value_type\":\"boolean\",\"order\":\"asc\"}}}]}}}")); assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("count(*){a->")); - assertThat(ee.output().get(1).toString(), startsWith("a{s->")); + assertThat(ee.output().get(0).toString(), startsWith("count(*){r}")); + assertThat(ee.output().get(1).toString(), startsWith("a{r}")); } public void testGroupKeyTypes_Integer() { @@ -310,8 +310,8 @@ public void testGroupKeyTypes_Integer() { "\"lang\":\"painless\",\"params\":{\"v0\":\"int\",\"v1\":10}},\"missing_bucket\":true," + "\"value_type\":\"long\",\"order\":\"asc\"}}}]}}}")); assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("count(*){a->")); - assertThat(ee.output().get(1).toString(), startsWith("a{s->")); + assertThat(ee.output().get(0).toString(), startsWith("count(*){r}")); + assertThat(ee.output().get(1).toString(), startsWith("a{r}")); } public void testGroupKeyTypes_Rational() { @@ -324,8 +324,8 @@ public void testGroupKeyTypes_Rational() { "\"lang\":\"painless\",\"params\":{\"v0\":\"int\"}},\"missing_bucket\":true," + "\"value_type\":\"double\",\"order\":\"asc\"}}}]}}}")); assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("count(*){a->")); - assertThat(ee.output().get(1).toString(), startsWith("a{s->")); + assertThat(ee.output().get(0).toString(), startsWith("count(*){r}")); + assertThat(ee.output().get(1).toString(), startsWith("a{r}")); } public void testGroupKeyTypes_String() { @@ -338,8 +338,8 @@ public void testGroupKeyTypes_String() { "\"lang\":\"painless\",\"params\":{\"v0\":\"keyword\"}},\"missing_bucket\":true," + "\"value_type\":\"string\",\"order\":\"asc\"}}}]}}}")); assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("count(*){a->")); - assertThat(ee.output().get(1).toString(), startsWith("a{s->")); + assertThat(ee.output().get(0).toString(), startsWith("count(*){r}#")); + assertThat(ee.output().get(1).toString(), startsWith("a{r}")); } public void testGroupKeyTypes_IP() { @@ -352,8 +352,8 @@ public void testGroupKeyTypes_IP() { "\"lang\":\"painless\",\"params\":{\"v0\":\"keyword\",\"v1\":\"IP\"}}," + "\"missing_bucket\":true,\"value_type\":\"ip\",\"order\":\"asc\"}}}]}}}")); assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("count(*){a->")); - assertThat(ee.output().get(1).toString(), startsWith("a{s->")); + assertThat(ee.output().get(0).toString(), startsWith("count(*){r}#")); + assertThat(ee.output().get(1).toString(), startsWith("a{r}")); } public void testGroupKeyTypes_DateTime() { @@ -367,8 +367,8 @@ public void testGroupKeyTypes_DateTime() { "\"v0\":\"date\",\"v1\":\"P1Y2M\",\"v2\":\"INTERVAL_YEAR_TO_MONTH\"}},\"missing_bucket\":true," + "\"value_type\":\"long\",\"order\":\"asc\"}}}]}}}")); assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("count(*){a->")); - assertThat(ee.output().get(1).toString(), startsWith("a{s->")); + assertThat(ee.output().get(0).toString(), startsWith("count(*){r}#")); + assertThat(ee.output().get(1).toString(), startsWith("a{r}")); } public void testConcatIsNotFoldedForNull() { @@ -378,7 +378,7 @@ public void testConcatIsNotFoldedForNull() { assertEquals(EmptyExecutable.class, le.executable().getClass()); EmptyExecutable ee = (EmptyExecutable) le.executable(); assertEquals(1, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#")); + assertThat(ee.output().get(0).toString(), startsWith("test.keyword{f}#")); } public void testFoldingOfPercentileSecondArgument() { @@ -386,9 +386,8 @@ public void testFoldingOfPercentileSecondArgument() { assertEquals(EsQueryExec.class, p.getClass()); EsQueryExec ee = (EsQueryExec) p; assertEquals(1, ee.output().size()); - assertEquals(AggregateFunctionAttribute.class, ee.output().get(0).getClass()); - AggregateFunctionAttribute afa = (AggregateFunctionAttribute) ee.output().get(0); - assertThat(afa.propertyPath(), endsWith("[3.0]")); + assertEquals(ReferenceAttribute.class, ee.output().get(0).getClass()); + assertTrue(ee.toString().contains("3.0")); } public void testFoldingOfPercentileRankSecondArgument() { @@ -396,9 +395,8 @@ public void testFoldingOfPercentileRankSecondArgument() { assertEquals(EsQueryExec.class, p.getClass()); EsQueryExec ee = (EsQueryExec) p; assertEquals(1, ee.output().size()); - assertEquals(AggregateFunctionAttribute.class, ee.output().get(0).getClass()); - AggregateFunctionAttribute afa = (AggregateFunctionAttribute) ee.output().get(0); - assertThat(afa.propertyPath(), endsWith("[3.0]")); + assertEquals(ReferenceAttribute.class, ee.output().get(0).getClass()); + assertTrue(ee.toString().contains("3.0")); } public void testFoldingOfPivot() { diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java index 36722e6e1d0f5..41da90ad3164f 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; import org.elasticsearch.xpack.sql.analysis.index.EsIndex; import org.elasticsearch.xpack.sql.analysis.index.IndexResolution; +import org.elasticsearch.xpack.sql.expression.Alias; import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.FieldAttribute; import org.elasticsearch.xpack.sql.expression.Literal; @@ -36,6 +37,7 @@ import org.elasticsearch.xpack.sql.plan.logical.Project; import org.elasticsearch.xpack.sql.plan.physical.EsQueryExec; import org.elasticsearch.xpack.sql.plan.physical.PhysicalPlan; +import org.elasticsearch.xpack.sql.planner.QueryFolder.FoldAggregate.GroupingContext; import org.elasticsearch.xpack.sql.planner.QueryTranslator.QueryTranslation; import org.elasticsearch.xpack.sql.querydsl.agg.AggFilter; import org.elasticsearch.xpack.sql.querydsl.agg.GroupByDateHistogram; @@ -437,7 +439,7 @@ public void testLikeConstructsNotSupported() { assertTrue(p instanceof Filter); Expression condition = ((Filter) p).condition(); SqlIllegalArgumentException ex = expectThrows(SqlIllegalArgumentException.class, () -> QueryTranslator.toQuery(condition, false)); - assertEquals("Scalar function [LTRIM(keyword)] not allowed (yet) as argument for LIKE", ex.getMessage()); + assertEquals("Scalar function [LTRIM(keyword)] not allowed (yet) as argument for LTRIM(keyword) like '%a%'", ex.getMessage()); } public void testRLikeConstructsNotSupported() { @@ -447,7 +449,7 @@ public void testRLikeConstructsNotSupported() { assertTrue(p instanceof Filter); Expression condition = ((Filter) p).condition(); SqlIllegalArgumentException ex = expectThrows(SqlIllegalArgumentException.class, () -> QueryTranslator.toQuery(condition, false)); - assertEquals("Scalar function [LTRIM(keyword)] not allowed (yet) as argument for RLIKE", ex.getMessage()); + assertEquals("Scalar function [LTRIM(keyword)] not allowed (yet) as argument for LTRIM(keyword) RLIKE '.*a.*'", ex.getMessage()); } public void testDifferentLikeAndNotLikePatterns() { @@ -592,7 +594,7 @@ public void testTranslateIsNullExpression_HavingClause_Painless() { AggFilter aggFilter = translation.aggFilter; assertEquals("InternalSqlScriptUtils.nullSafeFilter(InternalSqlScriptUtils.isNull(params.a0))", aggFilter.scriptTemplate().toString()); - assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int){a->")); + assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int)")); } public void testTranslateIsNotNullExpression_HavingClause_Painless() { @@ -605,7 +607,7 @@ public void testTranslateIsNotNullExpression_HavingClause_Painless() { AggFilter aggFilter = translation.aggFilter; assertEquals("InternalSqlScriptUtils.nullSafeFilter(InternalSqlScriptUtils.isNotNull(params.a0))", aggFilter.scriptTemplate().toString()); - assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int){a->")); + assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int)")); } public void testTranslateInExpression_WhereClause() { @@ -676,7 +678,7 @@ public void testTranslateInExpression_HavingClause_Painless() { AggFilter aggFilter = translation.aggFilter; assertEquals("InternalSqlScriptUtils.nullSafeFilter(InternalSqlScriptUtils.in(params.a0, params.v0))", aggFilter.scriptTemplate().toString()); - assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int){a->")); + assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int)")); assertThat(aggFilter.scriptTemplate().params().toString(), endsWith(", {v=[10, 20]}]")); } @@ -690,7 +692,7 @@ public void testTranslateInExpression_HavingClause_PainlessOneArg() { AggFilter aggFilter = translation.aggFilter; assertEquals("InternalSqlScriptUtils.nullSafeFilter(InternalSqlScriptUtils.in(params.a0, params.v0))", aggFilter.scriptTemplate().toString()); - assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int){a->")); + assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int)")); assertThat(aggFilter.scriptTemplate().params().toString(), endsWith(", {v=[10]}]")); } @@ -705,7 +707,7 @@ public void testTranslateInExpression_HavingClause_PainlessAndNullHandling() { AggFilter aggFilter = translation.aggFilter; assertEquals("InternalSqlScriptUtils.nullSafeFilter(InternalSqlScriptUtils.in(params.a0, params.v0))", aggFilter.scriptTemplate().toString()); - assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int){a->")); + assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int)")); assertThat(aggFilter.scriptTemplate().params().toString(), endsWith(", {v=[10, null, 20, 30]}]")); } @@ -724,7 +726,7 @@ public void testTranslateMathFunction_HavingClause_Painless() { assertEquals("InternalSqlScriptUtils.nullSafeFilter(InternalSqlScriptUtils.gt(InternalSqlScriptUtils." + operation.name().toLowerCase(Locale.ROOT) + "(params.a0),params.v0))", aggFilter.scriptTemplate().toString()); - assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int){a->")); + assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int)")); assertThat(aggFilter.scriptTemplate().params().toString(), endsWith(", {v=10}]")); } @@ -735,12 +737,12 @@ public void testTranslateRoundWithOneParameter() { assertEquals(1, ((Aggregate) p).groupings().size()); assertEquals(1, ((Aggregate) p).aggregates().size()); assertTrue(((Aggregate) p).groupings().get(0) instanceof Round); - assertTrue(((Aggregate) p).aggregates().get(0) instanceof Round); + assertTrue(((Alias) (((Aggregate) p).aggregates().get(0))).child() instanceof Round); Round groupingRound = (Round) ((Aggregate) p).groupings().get(0); assertEquals(1, groupingRound.children().size()); - QueryTranslator.GroupingContext groupingContext = QueryTranslator.groupBy(((Aggregate) p).groupings()); + GroupingContext groupingContext = QueryFolder.FoldAggregate.groupBy(((Aggregate) p).groupings()); assertNotNull(groupingContext); ScriptTemplate scriptTemplate = groupingContext.tail.script(); assertEquals("InternalSqlScriptUtils.round(InternalSqlScriptUtils.dateTimeChrono(InternalSqlScriptUtils.docValue(doc,params.v0), " @@ -756,14 +758,15 @@ public void testTranslateRoundWithTwoParameters() { assertEquals(1, ((Aggregate) p).groupings().size()); assertEquals(1, ((Aggregate) p).aggregates().size()); assertTrue(((Aggregate) p).groupings().get(0) instanceof Round); - assertTrue(((Aggregate) p).aggregates().get(0) instanceof Round); + assertTrue(((Aggregate) p).aggregates().get(0) instanceof Alias); + assertTrue(((Alias) (((Aggregate) p).aggregates().get(0))).child() instanceof Round); Round groupingRound = (Round) ((Aggregate) p).groupings().get(0); assertEquals(2, groupingRound.children().size()); assertTrue(groupingRound.children().get(1) instanceof Literal); assertEquals(-2, ((Literal) groupingRound.children().get(1)).value()); - QueryTranslator.GroupingContext groupingContext = QueryTranslator.groupBy(((Aggregate) p).groupings()); + GroupingContext groupingContext = QueryFolder.FoldAggregate.groupBy(((Aggregate) p).groupings()); assertNotNull(groupingContext); ScriptTemplate scriptTemplate = groupingContext.tail.script(); assertEquals("InternalSqlScriptUtils.round(InternalSqlScriptUtils.dateTimeChrono(InternalSqlScriptUtils.docValue(doc,params.v0), " @@ -783,7 +786,7 @@ public void testGroupByAndHavingWithFunctionOnTopOfAggregation() { assertEquals("InternalSqlScriptUtils.nullSafeFilter(InternalSqlScriptUtils.gt(InternalSqlScriptUtils.abs" + "(params.a0),params.v0))", aggFilter.scriptTemplate().toString()); - assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=MAX(int){a->")); + assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=MAX(int)")); assertThat(aggFilter.scriptTemplate().params().toString(), endsWith(", {v=10}]")); } @@ -895,7 +898,7 @@ public void testTranslateCoalesce_GroupBy_Painless() { assertTrue(p instanceof Aggregate); Expression condition = ((Aggregate) p).groupings().get(0); assertFalse(condition.foldable()); - QueryTranslator.GroupingContext groupingContext = QueryTranslator.groupBy(((Aggregate) p).groupings()); + GroupingContext groupingContext = QueryFolder.FoldAggregate.groupBy(((Aggregate) p).groupings()); assertNotNull(groupingContext); ScriptTemplate scriptTemplate = groupingContext.tail.script(); assertEquals("InternalSqlScriptUtils.coalesce([InternalSqlScriptUtils.docValue(doc,params.v0),params.v1])", @@ -908,7 +911,7 @@ public void testTranslateNullIf_GroupBy_Painless() { assertTrue(p instanceof Aggregate); Expression condition = ((Aggregate) p).groupings().get(0); assertFalse(condition.foldable()); - QueryTranslator.GroupingContext groupingContext = QueryTranslator.groupBy(((Aggregate) p).groupings()); + GroupingContext groupingContext = QueryFolder.FoldAggregate.groupBy(((Aggregate) p).groupings()); assertNotNull(groupingContext); ScriptTemplate scriptTemplate = groupingContext.tail.script(); assertEquals("InternalSqlScriptUtils.nullif(InternalSqlScriptUtils.docValue(doc,params.v0),params.v1)", @@ -921,7 +924,7 @@ public void testTranslateCase_GroupBy_Painless() { assertTrue(p instanceof Aggregate); Expression condition = ((Aggregate) p).groupings().get(0); assertFalse(condition.foldable()); - QueryTranslator.GroupingContext groupingContext = QueryTranslator.groupBy(((Aggregate) p).groupings()); + GroupingContext groupingContext = QueryFolder.FoldAggregate.groupBy(((Aggregate) p).groupings()); assertNotNull(groupingContext); ScriptTemplate scriptTemplate = groupingContext.tail.script(); assertEquals("InternalSqlScriptUtils.caseFunction([InternalSqlScriptUtils.gt(InternalSqlScriptUtils.docValue(" + "" + @@ -936,7 +939,7 @@ public void testTranslateIif_GroupBy_Painless() { assertTrue(p instanceof Aggregate); Expression condition = ((Aggregate) p).groupings().get(0); assertFalse(condition.foldable()); - QueryTranslator.GroupingContext groupingContext = QueryTranslator.groupBy(((Aggregate) p).groupings()); + GroupingContext groupingContext = QueryFolder.FoldAggregate.groupBy(((Aggregate) p).groupings()); assertNotNull(groupingContext); ScriptTemplate scriptTemplate = groupingContext.tail.script(); assertEquals("InternalSqlScriptUtils.caseFunction([InternalSqlScriptUtils.gt(" + @@ -1066,8 +1069,8 @@ public void testCountAndCountDistinctFolding() { assertEquals(EsQueryExec.class, p.getClass()); EsQueryExec ee = (EsQueryExec) p; assertEquals(2, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("dkey{a->")); - assertThat(ee.output().get(1).toString(), startsWith("key{a->")); + assertThat(ee.output().get(0).toString(), startsWith("dkey{r}")); + assertThat(ee.output().get(1).toString(), startsWith("key{r}")); Collection subAggs = ee.queryContainer().aggs().asAggBuilder().getSubAggregations(); assertEquals(2, subAggs.size()); @@ -1092,12 +1095,12 @@ public void testAllCountVariantsWithHavingGenerateCorrectAggregations() { assertEquals(EsQueryExec.class, p.getClass()); EsQueryExec ee = (EsQueryExec) p; assertEquals(6, ee.output().size()); - assertThat(ee.output().get(0).toString(), startsWith("AVG(int){a->")); - assertThat(ee.output().get(1).toString(), startsWith("ln{a->")); - assertThat(ee.output().get(2).toString(), startsWith("dln{a->")); - assertThat(ee.output().get(3).toString(), startsWith("fn{a->")); - assertThat(ee.output().get(4).toString(), startsWith("dfn{a->")); - assertThat(ee.output().get(5).toString(), startsWith("ccc{a->")); + assertThat(ee.output().get(0).toString(), startsWith("AVG(int){r}")); + assertThat(ee.output().get(1).toString(), startsWith("ln{r}")); + assertThat(ee.output().get(2).toString(), startsWith("dln{r}")); + assertThat(ee.output().get(3).toString(), startsWith("fn{r}")); + assertThat(ee.output().get(4).toString(), startsWith("dfn{r}")); + assertThat(ee.output().get(5).toString(), startsWith("ccc{r}")); Collection subAggs = ee.queryContainer().aggs().asAggBuilder().getSubAggregations(); assertEquals(5, subAggs.size()); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainerTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainerTests.java index a23dc8a3f2784..432be5fea4a8f 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainerTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainerTests.java @@ -8,7 +8,8 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.sql.expression.Alias; import org.elasticsearch.xpack.sql.expression.Attribute; -import org.elasticsearch.xpack.sql.expression.ExpressionId; +import org.elasticsearch.xpack.sql.expression.AttributeMap; +import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.FieldAttribute; import org.elasticsearch.xpack.sql.querydsl.query.BoolQuery; import org.elasticsearch.xpack.sql.querydsl.query.MatchAll; @@ -81,11 +82,11 @@ public void testColumnMaskShouldDuplicateSameAttributes() { Attribute fourth = new FieldAttribute(Source.EMPTY, "fourth", esField); Alias firstAliased = new Alias(Source.EMPTY, "firstAliased", first); - Map aliasesMap = new LinkedHashMap<>(); - aliasesMap.put(firstAliased.id(), first); + Map aliasesMap = new LinkedHashMap<>(); + aliasesMap.put(firstAliased.toAttribute(), first); QueryContainer queryContainer = new QueryContainer() - .withAliases(aliasesMap) + .withAliases(new AttributeMap<>(aliasesMap)) .addColumn(third) .addColumn(first) .addColumn(fourth) From 22433e91c85ce2d20a5836f2af9dcc1d609aee70 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 6 Dec 2019 17:43:25 +0100 Subject: [PATCH 099/686] Fix TimedRunnable Executing onAfter Twice (#49910) If we have a nested `AbstractRunnable` inside of `TimedRunnable` it's executed twice on `run` (once when its own `run` method is invoked and once when the `onAfter` in the `TimedRunnable` is executed). Simply removing the `onAfter` override in `TimedRunnable` makes sure that the `onAfter` is only called once by the `run` on the nested `AbstractRunnable` itself. Same was done for `onFailure` as it was double-triggering as well on exceptions in the inner `onFailure`. --- .../common/util/concurrent/TimedRunnable.java | 13 +---- .../util/concurrent/TimedRunnableTests.java | 55 ++++++++++++++++--- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/TimedRunnable.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/TimedRunnable.java index f2de68453a6c2..075556e730134 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/TimedRunnable.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/TimedRunnable.java @@ -57,21 +57,10 @@ public void onRejection(final Exception e) { } } - @Override - public void onAfter() { - if (original instanceof AbstractRunnable) { - ((AbstractRunnable) original).onAfter(); - } - } - @Override public void onFailure(final Exception e) { this.failedOrRejected = true; - if (original instanceof AbstractRunnable) { - ((AbstractRunnable) original).onFailure(e); - } else { - ExceptionsHelper.reThrowIfNotNull(e); - } + ExceptionsHelper.reThrowIfNotNull(e); } @Override diff --git a/server/src/test/java/org/elasticsearch/common/util/concurrent/TimedRunnableTests.java b/server/src/test/java/org/elasticsearch/common/util/concurrent/TimedRunnableTests.java index 9cd84617273ea..8725a4734cb19 100644 --- a/server/src/test/java/org/elasticsearch/common/util/concurrent/TimedRunnableTests.java +++ b/server/src/test/java/org/elasticsearch/common/util/concurrent/TimedRunnableTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.common.util.concurrent; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.test.ESTestCase; import java.util.concurrent.RejectedExecutionException; @@ -33,7 +34,6 @@ public void testTimedRunnableDelegatesToAbstractRunnable() { final boolean isForceExecution = randomBoolean(); final AtomicBoolean onAfter = new AtomicBoolean(); final AtomicReference onRejection = new AtomicReference<>(); - final AtomicReference onFailure = new AtomicReference<>(); final AtomicBoolean doRun = new AtomicBoolean(); final AbstractRunnable runnable = new AbstractRunnable() { @@ -54,7 +54,6 @@ public void onRejection(final Exception e) { @Override public void onFailure(final Exception e) { - onFailure.set(e); } @Override @@ -67,19 +66,13 @@ protected void doRun() throws Exception { assertThat(timedRunnable.isForceExecution(), equalTo(isForceExecution)); - timedRunnable.onAfter(); - assertTrue(onAfter.get()); - final Exception rejection = new RejectedExecutionException(); timedRunnable.onRejection(rejection); assertThat(onRejection.get(), equalTo(rejection)); - final Exception failure = new Exception(); - timedRunnable.onFailure(failure); - assertThat(onFailure.get(), equalTo(failure)); - timedRunnable.run(); assertTrue(doRun.get()); + assertTrue(onAfter.get()); } public void testTimedRunnableDelegatesRunInFailureCase() { @@ -144,4 +137,48 @@ public void testTimedRunnableRethrowsRejectionWhenNotAbstractRunnable() { assertSame(exception, thrown); } + public void testTimedRunnableExecutesNestedOnAfterOnce() { + final AtomicBoolean afterRan = new AtomicBoolean(false); + new TimedRunnable(new AbstractRunnable() { + + @Override + public void onFailure(final Exception e) { + } + + @Override + protected void doRun() { + } + + @Override + public void onAfter() { + if (afterRan.compareAndSet(false, true) == false) { + fail("onAfter should have only been called once"); + } + } + }).run(); + assertTrue(afterRan.get()); + } + + public void testNestedOnFailureTriggersOnlyOnce() { + final Exception expectedException = new RuntimeException(); + final AtomicBoolean onFailureRan = new AtomicBoolean(false); + RuntimeException thrown = expectThrows(RuntimeException.class, () -> new TimedRunnable(new AbstractRunnable() { + + @Override + public void onFailure(Exception e) { + if (onFailureRan.compareAndSet(false, true) == false) { + fail("onFailure should have only been called once"); + } + ExceptionsHelper.reThrowIfNotNull(e); + } + + @Override + protected void doRun() throws Exception { + throw expectedException; + } + + }).run()); + assertTrue(onFailureRan.get()); + assertSame(thrown, expectedException); + } } From 0665944cc424377e9d694f2931d8305293f33580 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Fri, 6 Dec 2019 17:04:09 +0000 Subject: [PATCH 100/686] [TEST] Mute ConnectionManagerTests.testConcurrentConnectsAndDisconnects Due to https://github.com/elastic/elasticsearch/issues/49903 --- .../java/org/elasticsearch/transport/ConnectionManagerTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java b/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java index f34d39ae7252a..37c0d92612fdc 100644 --- a/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java +++ b/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java @@ -124,6 +124,7 @@ public void onNodeDisconnected(DiscoveryNode node, Transport.Connection connecti assertEquals(1, nodeDisconnectedCount.get()); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/49903") public void testConcurrentConnectsAndDisconnects() throws BrokenBarrierException, InterruptedException { DiscoveryNode node = new DiscoveryNode("", new TransportAddress(InetAddress.getLoopbackAddress(), 0), Version.CURRENT); Transport.Connection connection = new TestConnect(node); From 8d82517795c03f36288cd9ae226063d1860adaaf Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Fri, 6 Dec 2019 11:34:25 -0700 Subject: [PATCH 101/686] Refactor IndexLifecycleRunner to split state modification (#49891) This commit refactors the `IndexLifecycleRunner` to split out and consolidate the number of methods that change state from within ILM. It adds a new class `IndexLifecycleTransition` that contains a number of static methods used to modify ILM's state. These methods all return new cluster states rather than making changes themselves (they can be thought of as helpers for modifying ILM state). Rather than having multiple ways to move an index to a particular step (like `moveClusterStateToStep`, `moveClusterStateToNextStep`, `moveClusterStateToPreviouslyFailedStep`, etc (there are others)) this now consolidates those into three with (hopefully) useful names: - `moveClusterStateToStep` - `moveClusterStateToErrorStep` - `moveClusterStateToPreviouslyFailedStep` In the move, I was also able to consolidate duplicate or redundant arguments to these functions. Prior to this commit there were many calls that provided duplicate information (both `IndexMetaData` and `LifecycleExecutionState` for example) where the duplicate argument could be derived from a previous argument with no problems. With this split, `IndexLifecycleRunner` now contains the methods used to actually run steps as well as the methods that kick off cluster state updates for state transitions. `IndexLifecycleTransition` contains only the helpers for constructing new states from given scenarios. This also adds Javadocs to all methods in both `IndexLifecycleRunner` and `IndexLifecycleTransition` (this accounts for almost all of the increase in code lines for this commit). It also makes all methods be as restrictive in visibility, to limit the scope of where they are used. This refactoring is part of work towards capturing actions and transitions that ILM makes, by consolidating and simplifying the places we make state changes, it will make adding operation auditing easier. --- .../core/ilm/LifecycleExecutionState.java | 32 + .../ilm/LifecycleExecutionStateTests.java | 63 ++ .../test/ilm/40_explain_lifecycle.yml | 12 +- .../xpack/ilm/ExecuteStepsUpdateTask.java | 14 +- .../xpack/ilm/IndexLifecycleRunner.java | 406 ++------ .../xpack/ilm/IndexLifecycleService.java | 32 +- .../xpack/ilm/IndexLifecycleTransition.java | 342 +++++++ .../xpack/ilm/MoveToErrorStepUpdateTask.java | 5 +- .../xpack/ilm/MoveToNextStepUpdateTask.java | 9 +- .../xpack/ilm/SetStepInfoUpdateTask.java | 4 +- .../ilm/action/TransportMoveToStepAction.java | 2 +- ...sportRemoveIndexLifecyclePolicyAction.java | 4 +- .../ilm/ExecuteStepsUpdateTaskTests.java | 12 +- .../xpack/ilm/IndexLifecycleRunnerTests.java | 911 ++---------------- .../ilm/IndexLifecycleTransitionTests.java | 784 +++++++++++++++ .../ilm/MoveToErrorStepUpdateTaskTests.java | 2 +- .../ilm/MoveToNextStepUpdateTaskTests.java | 33 +- .../xpack/ilm/SetStepInfoUpdateTaskTests.java | 2 +- 18 files changed, 1446 insertions(+), 1223 deletions(-) create mode 100644 x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransition.java create mode 100644 x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java index abc0bd7731b5f..304982381803e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java @@ -8,6 +8,8 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; import java.util.Collections; import java.util.HashMap; @@ -78,6 +80,31 @@ public static LifecycleExecutionState fromIndexMetadata(IndexMetaData indexMetaD return fromCustomMetadata(customData); } + /** + * Retrieves the current {@link Step.StepKey} from the lifecycle state. Note that + * it is illegal for the step to be set with the phase and/or action unset, + * or for the step to be unset with the phase and/or action set. All three + * settings must be either present or missing. + * + * @param lifecycleState the index custom data to extract the {@link Step.StepKey} from. + */ + @Nullable + public static Step.StepKey getCurrentStepKey(LifecycleExecutionState lifecycleState) { + Objects.requireNonNull(lifecycleState, "cannot determine current step key as lifecycle state is null"); + String currentPhase = lifecycleState.getPhase(); + String currentAction = lifecycleState.getAction(); + String currentStep = lifecycleState.getStep(); + if (Strings.isNullOrEmpty(currentStep)) { + assert Strings.isNullOrEmpty(currentPhase) : "Current phase is not empty: " + currentPhase; + assert Strings.isNullOrEmpty(currentAction) : "Current action is not empty: " + currentAction; + return null; + } else { + assert Strings.isNullOrEmpty(currentPhase) == false; + assert Strings.isNullOrEmpty(currentAction) == false; + return new Step.StepKey(currentPhase, currentAction, currentStep); + } + } + public static Builder builder() { return new Builder(); } @@ -278,6 +305,11 @@ public int hashCode() { getStepInfo(), getPhaseDefinition(), getLifecycleDate(), getPhaseTime(), getActionTime(), getStepTime()); } + @Override + public String toString() { + return asMap().toString(); + } + public static class Builder { private String phase; private String action; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionStateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionStateTests.java index e6e9d5ead0fb1..7a7782fb389ca 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionStateTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionStateTests.java @@ -71,6 +71,69 @@ public void testEqualsAndHashcode() { LifecycleExecutionStateTests::mutate); } + public void testGetCurrentStepKey() { + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + Step.StepKey stepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState.build()); + assertNull(stepKey); + + String phase = randomAlphaOfLength(20); + String action = randomAlphaOfLength(20); + String step = randomAlphaOfLength(20); + LifecycleExecutionState.Builder lifecycleState2 = LifecycleExecutionState.builder(); + lifecycleState2.setPhase(phase); + lifecycleState2.setAction(action); + lifecycleState2.setStep(step); + stepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState2.build()); + assertNotNull(stepKey); + assertEquals(phase, stepKey.getPhase()); + assertEquals(action, stepKey.getAction()); + assertEquals(step, stepKey.getName()); + + phase = randomAlphaOfLength(20); + action = randomAlphaOfLength(20); + step = null; + LifecycleExecutionState.Builder lifecycleState3 = LifecycleExecutionState.builder(); + lifecycleState3.setPhase(phase); + lifecycleState3.setAction(action); + lifecycleState3.setStep(step); + AssertionError error3 = expectThrows(AssertionError.class, + () -> LifecycleExecutionState.getCurrentStepKey(lifecycleState3.build())); + assertEquals("Current phase is not empty: " + phase, error3.getMessage()); + + phase = null; + action = randomAlphaOfLength(20); + step = null; + LifecycleExecutionState.Builder lifecycleState4 = LifecycleExecutionState.builder(); + lifecycleState4.setPhase(phase); + lifecycleState4.setAction(action); + lifecycleState4.setStep(step); + AssertionError error4 = expectThrows(AssertionError.class, + () -> LifecycleExecutionState.getCurrentStepKey(lifecycleState4.build())); + assertEquals("Current action is not empty: " + action, error4.getMessage()); + + phase = null; + action = randomAlphaOfLength(20); + step = randomAlphaOfLength(20); + LifecycleExecutionState.Builder lifecycleState5 = LifecycleExecutionState.builder(); + lifecycleState5.setPhase(phase); + lifecycleState5.setAction(action); + lifecycleState5.setStep(step); + AssertionError error5 = expectThrows(AssertionError.class, + () -> LifecycleExecutionState.getCurrentStepKey(lifecycleState5.build())); + assertNull(error5.getMessage()); + + phase = null; + action = null; + step = randomAlphaOfLength(20); + LifecycleExecutionState.Builder lifecycleState6 = LifecycleExecutionState.builder(); + lifecycleState6.setPhase(phase); + lifecycleState6.setAction(action); + lifecycleState6.setStep(step); + AssertionError error6 = expectThrows(AssertionError.class, + () -> LifecycleExecutionState.getCurrentStepKey(lifecycleState6.build())); + assertNull(error6.getMessage()); + } + private static LifecycleExecutionState mutate(LifecycleExecutionState toMutate) { LifecycleExecutionState.Builder newState = LifecycleExecutionState.builder(toMutate); boolean changed = false; diff --git a/x-pack/plugin/ilm/qa/rest/src/test/resources/rest-api-spec/test/ilm/40_explain_lifecycle.yml b/x-pack/plugin/ilm/qa/rest/src/test/resources/rest-api-spec/test/ilm/40_explain_lifecycle.yml index c91f98fb18bd2..fc9b187923267 100644 --- a/x-pack/plugin/ilm/qa/rest/src/test/resources/rest-api-spec/test/ilm/40_explain_lifecycle.yml +++ b/x-pack/plugin/ilm/qa/rest/src/test/resources/rest-api-spec/test/ilm/40_explain_lifecycle.yml @@ -115,9 +115,9 @@ teardown: - match: { indices.my_index.step: "complete" } - is_true: indices.my_index.phase_time_millis - is_true: indices.my_index.age + - is_true: indices.my_index.phase_execution - is_false: indices.my_index.failed_step - is_false: indices.my_index.step_info - - is_false: indices.my_index.phase_execution - is_false: indices.my_index2 - is_false: indices.another_index @@ -139,9 +139,9 @@ teardown: - match: { indices.my_index.step: "complete" } - is_true: indices.my_index.phase_time_millis - is_true: indices.my_index.age + - is_true: indices.my_index.phase_execution - is_false: indices.my_index.failed_step - is_false: indices.my_index.step_info - - is_false: indices.my_index.phase_execution - is_true: indices.my_index2.managed - match: { indices.my_index2.index: "my_index2" } @@ -151,9 +151,9 @@ teardown: - match: { indices.my_index2.step: "complete" } - is_true: indices.my_index2.phase_time_millis - is_true: indices.my_index2.age + - is_true: indices.my_index2.phase_execution - is_false: indices.my_index2.failed_step - is_false: indices.my_index2.step_info - - is_false: indices.my_index2.phase_execution - is_false: indices.another_index - is_false: indices.unmanaged_index @@ -178,9 +178,9 @@ teardown: - match: { indices.my_index.step: "complete" } - is_true: indices.my_index.phase_time_millis - is_true: indices.my_index.age + - is_true: indices.my_index.phase_execution - is_false: indices.my_index.failed_step - is_false: indices.my_index.step_info - - is_false: indices.my_index.phase_execution - is_true: indices.my_index2.managed - match: { indices.my_index2.index: "my_index2" } @@ -190,9 +190,9 @@ teardown: - match: { indices.my_index2.step: "complete" } - is_true: indices.my_index2.phase_time_millis - is_true: indices.my_index2.age + - is_true: indices.my_index2.phase_execution - is_false: indices.my_index2.failed_step - is_false: indices.my_index2.step_info - - is_false: indices.my_index2.phase_execution - is_true: indices.another_index.managed - match: { indices.another_index.index: "another_index" } @@ -202,9 +202,9 @@ teardown: - match: { indices.another_index.step: "complete" } - is_true: indices.another_index.phase_time_millis - is_true: indices.another_index.age + - is_true: indices.another_index.phase_execution - is_false: indices.another_index.failed_step - is_false: indices.another_index.step_info - - is_false: indices.another_index.phase_execution - match: { indices.unmanaged_index.index: "unmanaged_index" } - is_false: indices.unmanaged_index.managed diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java index 9b269b0695473..46364f7cb4021 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java @@ -16,7 +16,6 @@ import org.elasticsearch.index.Index; import org.elasticsearch.xpack.core.ilm.ClusterStateActionStep; import org.elasticsearch.xpack.core.ilm.ClusterStateWaitStep; -import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; import org.elasticsearch.xpack.core.ilm.Step; import org.elasticsearch.xpack.core.ilm.TerminalPolicyStep; @@ -78,8 +77,7 @@ public ClusterState execute(final ClusterState currentState) throws IOException // This index doesn't exist any more, there's nothing to execute currently return currentState; } - Step registeredCurrentStep = IndexLifecycleRunner.getCurrentStep(policyStepsRegistry, policy, indexMetaData, - LifecycleExecutionState.fromIndexMetadata(indexMetaData)); + Step registeredCurrentStep = IndexLifecycleRunner.getCurrentStep(policyStepsRegistry, policy, indexMetaData); if (currentStep.equals(registeredCurrentStep)) { ClusterState state = currentState; // We can do cluster state steps all together until we @@ -103,8 +101,8 @@ public ClusterState execute(final ClusterState currentState) throws IOException return state; } else { logger.trace("[{}] moving cluster state to next step [{}]", index.getName(), nextStepKey); - state = IndexLifecycleRunner.moveClusterStateToNextStep(index, state, currentStep.getKey(), - nextStepKey, nowSupplier, false); + state = IndexLifecycleTransition.moveClusterStateToStep(index, state, nextStepKey, nowSupplier, + policyStepsRegistry, false); } } else { // set here to make sure that the clusterProcessed knows to execute the @@ -130,8 +128,8 @@ public ClusterState execute(final ClusterState currentState) throws IOException if (currentStep.getNextStepKey() == null) { return state; } else { - state = IndexLifecycleRunner.moveClusterStateToNextStep(index, state, currentStep.getKey(), - currentStep.getNextStepKey(), nowSupplier, false); + state = IndexLifecycleTransition.moveClusterStateToStep(index, state, + currentStep.getNextStepKey(), nowSupplier, policyStepsRegistry,false); } } else { logger.trace("[{}] condition not met ({}) [{}], returning existing state", @@ -145,7 +143,7 @@ public ClusterState execute(final ClusterState currentState) throws IOException if (stepInfo == null) { return state; } else { - return IndexLifecycleRunner.addStepInfoToClusterState(index, state, stepInfo); + return IndexLifecycleTransition.addStepInfoToClusterState(index, state, stepInfo); } } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java index 6ee59368ae7a5..5d64c35498a3d 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java @@ -8,22 +8,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.action.support.TransportAction; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateObserver; import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentObject; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.Index; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ilm.AsyncActionStep; @@ -31,46 +22,45 @@ import org.elasticsearch.xpack.core.ilm.ClusterStateActionStep; import org.elasticsearch.xpack.core.ilm.ClusterStateWaitStep; import org.elasticsearch.xpack.core.ilm.ErrorStep; -import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; -import org.elasticsearch.xpack.core.ilm.InitializePolicyContextStep; import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; -import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; -import org.elasticsearch.xpack.core.ilm.LifecycleSettings; -import org.elasticsearch.xpack.core.ilm.Phase; import org.elasticsearch.xpack.core.ilm.PhaseCompleteStep; -import org.elasticsearch.xpack.core.ilm.PhaseExecutionInfo; -import org.elasticsearch.xpack.core.ilm.RolloverAction; import org.elasticsearch.xpack.core.ilm.Step; import org.elasticsearch.xpack.core.ilm.Step.StepKey; import org.elasticsearch.xpack.core.ilm.TerminalPolicyStep; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.function.BiFunction; import java.util.function.LongSupplier; -import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE; -import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.elasticsearch.xpack.core.ilm.LifecycleSettings.LIFECYCLE_ORIGINATION_DATE; -public class IndexLifecycleRunner { +class IndexLifecycleRunner { private static final Logger logger = LogManager.getLogger(IndexLifecycleRunner.class); - private static final ToXContent.Params STACKTRACE_PARAMS = - new ToXContent.MapParams(Collections.singletonMap(REST_EXCEPTION_SKIP_STACK_TRACE, "false")); private final ThreadPool threadPool; private PolicyStepsRegistry stepRegistry; private ClusterService clusterService; private LongSupplier nowSupplier; - public IndexLifecycleRunner(PolicyStepsRegistry stepRegistry, ClusterService clusterService, - ThreadPool threadPool, LongSupplier nowSupplier) { + IndexLifecycleRunner(PolicyStepsRegistry stepRegistry, ClusterService clusterService, + ThreadPool threadPool, LongSupplier nowSupplier) { this.stepRegistry = stepRegistry; this.clusterService = clusterService; this.nowSupplier = nowSupplier; this.threadPool = threadPool; } + /** + * Retrieve the index's current step. + */ + static Step getCurrentStep(PolicyStepsRegistry stepRegistry, String policy, IndexMetaData indexMetaData) { + LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); + StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); + logger.trace("[{}] retrieved current step key: {}", indexMetaData.getIndex().getName(), currentStepKey); + if (currentStepKey == null) { + return stepRegistry.getFirstStep(policy); + } else { + return stepRegistry.getStep(indexMetaData, currentStepKey); + } + } + /** * Return true or false depending on whether the index is ready to be in {@code phase} */ @@ -101,12 +91,12 @@ boolean isReadyToTransitionToThisPhase(final String policy, final IndexMetaData * Run the current step, only if it is an asynchronous wait step. These * wait criteria are checked periodically from the ILM scheduler */ - public void runPeriodicStep(String policy, IndexMetaData indexMetaData) { + void runPeriodicStep(String policy, IndexMetaData indexMetaData) { String index = indexMetaData.getIndex().getName(); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); final Step currentStep; try { - currentStep = getCurrentStep(stepRegistry, policy, indexMetaData, lifecycleState); + currentStep = getCurrentStep(stepRegistry, policy, indexMetaData); } catch (Exception e) { markPolicyRetrievalError(policy, indexMetaData.getIndex(), lifecycleState, e); return; @@ -118,7 +108,7 @@ public void runPeriodicStep(String policy, IndexMetaData indexMetaData) { return; } else { logger.error("current step [{}] for index [{}] with policy [{}] is not recognized", - getCurrentStepKey(lifecycleState), index, policy); + LifecycleExecutionState.getCurrentStepKey(lifecycleState), index, policy); return; } } @@ -163,6 +153,11 @@ public void onFailure(Exception e) { } } + /** + * Given the policy and index metadata for an index, this moves the index's + * execution state to the previously failed step, incrementing the retry + * counter. + */ void onErrorMaybeRetryFailedStep(String policy, IndexMetaData indexMetaData) { String index = indexMetaData.getIndex().getName(); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); @@ -181,7 +176,8 @@ void onErrorMaybeRetryFailedStep(String policy, IndexMetaData indexMetaData) { clusterService.submitStateUpdateTask("ilm-retry-failed-step", new ClusterStateUpdateTask() { @Override public ClusterState execute(ClusterState currentState) { - return moveClusterStateToPreviouslyFailedStep(currentState, index, true); + return IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(currentState, index, + nowSupplier, stepRegistry, true); } @Override @@ -198,19 +194,19 @@ public void onFailure(String source, Exception e) { /** * If the current step (matching the expected step key) is an asynchronous action step, run it */ - public void maybeRunAsyncAction(ClusterState currentState, IndexMetaData indexMetaData, String policy, StepKey expectedStepKey) { + void maybeRunAsyncAction(ClusterState currentState, IndexMetaData indexMetaData, String policy, StepKey expectedStepKey) { String index = indexMetaData.getIndex().getName(); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); final Step currentStep; try { - currentStep = getCurrentStep(stepRegistry, policy, indexMetaData, lifecycleState); + currentStep = getCurrentStep(stepRegistry, policy, indexMetaData); } catch (Exception e) { markPolicyRetrievalError(policy, indexMetaData.getIndex(), lifecycleState, e); return; } if (currentStep == null) { logger.warn("current step [{}] for index [{}] with policy [{}] is not recognized", - getCurrentStepKey(lifecycleState), index, policy); + LifecycleExecutionState.getCurrentStepKey(lifecycleState), index, policy); return; } @@ -247,12 +243,12 @@ public void onFailure(Exception e) { * Run the current step that either waits for index age, or updates/waits-on cluster state. * Invoked after the cluster state has been changed */ - public void runPolicyAfterStateChange(String policy, IndexMetaData indexMetaData) { + void runPolicyAfterStateChange(String policy, IndexMetaData indexMetaData) { String index = indexMetaData.getIndex().getName(); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); final Step currentStep; try { - currentStep = getCurrentStep(stepRegistry, policy, indexMetaData, lifecycleState); + currentStep = getCurrentStep(stepRegistry, policy, indexMetaData); } catch (Exception e) { markPolicyRetrievalError(policy, indexMetaData.getIndex(), lifecycleState, e); return; @@ -263,7 +259,7 @@ public void runPolicyAfterStateChange(String policy, IndexMetaData indexMetaData return; } else { logger.error("current step [{}] for index [{}] with policy [{}] is not recognized", - getCurrentStepKey(lifecycleState), index, policy); + LifecycleExecutionState.getCurrentStepKey(lifecycleState), index, policy); return; } } @@ -293,336 +289,58 @@ public void runPolicyAfterStateChange(String policy, IndexMetaData indexMetaData } /** - * Retrieves the current {@link StepKey} from the index settings. Note that - * it is illegal for the step to be set with the phase and/or action unset, - * or for the step to be unset with the phase and/or action set. All three - * settings must be either present or missing. - * - * @param lifecycleState the index custom data to extract the {@link StepKey} from. - */ - public static StepKey getCurrentStepKey(LifecycleExecutionState lifecycleState) { - String currentPhase = lifecycleState.getPhase(); - String currentAction = lifecycleState.getAction(); - String currentStep = lifecycleState.getStep(); - if (Strings.isNullOrEmpty(currentStep)) { - assert Strings.isNullOrEmpty(currentPhase) : "Current phase is not empty: " + currentPhase; - assert Strings.isNullOrEmpty(currentAction) : "Current action is not empty: " + currentAction; - return null; - } else { - assert Strings.isNullOrEmpty(currentPhase) == false; - assert Strings.isNullOrEmpty(currentAction) == false; - return new StepKey(currentPhase, currentAction, currentStep); - } - } - - static Step getCurrentStep(PolicyStepsRegistry stepRegistry, String policy, IndexMetaData indexMetaData, - LifecycleExecutionState lifecycleState) { - StepKey currentStepKey = getCurrentStepKey(lifecycleState); - logger.trace("[{}] retrieved current step key: {}", indexMetaData.getIndex().getName(), currentStepKey); - if (currentStepKey == null) { - return stepRegistry.getFirstStep(policy); - } else { - return stepRegistry.getStep(indexMetaData, currentStepKey); - } - } - - /** - * This method is intended for handling moving to different steps from {@link TransportAction} executions. - * For this reason, it is reasonable to throw {@link IllegalArgumentException} when state is not as expected. - * - * @param indexName The index whose step is to change - * @param currentState The current {@link ClusterState} - * @param currentStepKey The current {@link StepKey} found for the index in the current cluster state - * @param nextStepKey The next step to move the index into - * @param nowSupplier The current-time supplier for updating when steps changed - * @param stepRegistry The steps registry to check a step-key's existence in the index's current policy - * @return The updated cluster state where the index moved to nextStepKey - */ - static ClusterState moveClusterStateToStep(String indexName, ClusterState currentState, StepKey currentStepKey, - StepKey nextStepKey, LongSupplier nowSupplier, - PolicyStepsRegistry stepRegistry) { - IndexMetaData idxMeta = currentState.getMetaData().index(indexName); - validateTransition(idxMeta, currentStepKey, nextStepKey, stepRegistry); - - Settings indexSettings = idxMeta.getSettings(); - String policy = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexSettings); - logger.info("moving index [{}] from [{}] to [{}] in policy [{}]", - indexName, currentStepKey, nextStepKey, policy); - - return IndexLifecycleRunner.moveClusterStateToNextStep(idxMeta.getIndex(), currentState, currentStepKey, - nextStepKey, nowSupplier, true); - } - - static void validateTransition(IndexMetaData idxMeta, StepKey currentStepKey, StepKey nextStepKey, PolicyStepsRegistry stepRegistry) { - String indexName = idxMeta.getIndex().getName(); - Settings indexSettings = idxMeta.getSettings(); - String indexPolicySetting = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexSettings); - - // policy could be updated in-between execution - if (Strings.isNullOrEmpty(indexPolicySetting)) { - throw new IllegalArgumentException("index [" + indexName + "] is not associated with an Index Lifecycle Policy"); - } - - LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(idxMeta); - if (currentStepKey.equals(IndexLifecycleRunner.getCurrentStepKey(lifecycleState)) == false) { - throw new IllegalArgumentException("index [" + indexName + "] is not on current step [" + currentStepKey + "]"); - } - - if (stepRegistry.stepExists(indexPolicySetting, nextStepKey) == false) { - throw new IllegalArgumentException("step [" + nextStepKey + "] for index [" + idxMeta.getIndex().getName() + - "] with policy [" + indexPolicySetting + "] does not exist"); - } - } - - static ClusterState moveClusterStateToNextStep(Index index, ClusterState clusterState, StepKey currentStep, StepKey nextStep, - LongSupplier nowSupplier, boolean forcePhaseDefinitionRefresh) { - IndexMetaData idxMeta = clusterState.getMetaData().index(index); - IndexLifecycleMetadata ilmMeta = clusterState.metaData().custom(IndexLifecycleMetadata.TYPE); - LifecyclePolicyMetadata policyMetadata = ilmMeta.getPolicyMetadatas() - .get(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(idxMeta.getSettings())); - LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(idxMeta); - LifecycleExecutionState newLifecycleState = moveExecutionStateToNextStep(policyMetadata, - lifecycleState, currentStep, nextStep, nowSupplier, forcePhaseDefinitionRefresh); - ClusterState.Builder newClusterStateBuilder = newClusterStateWithLifecycleState(index, clusterState, newLifecycleState); - - return newClusterStateBuilder.build(); - } - - static ClusterState moveClusterStateToErrorStep(Index index, ClusterState clusterState, StepKey currentStep, Exception cause, - LongSupplier nowSupplier, - BiFunction stepLookupFunction) throws IOException { - IndexMetaData idxMeta = clusterState.getMetaData().index(index); - IndexLifecycleMetadata ilmMeta = clusterState.metaData().custom(IndexLifecycleMetadata.TYPE); - LifecyclePolicyMetadata policyMetadata = ilmMeta.getPolicyMetadatas() - .get(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(idxMeta.getSettings())); - XContentBuilder causeXContentBuilder = JsonXContent.contentBuilder(); - causeXContentBuilder.startObject(); - ElasticsearchException.generateThrowableXContent(causeXContentBuilder, STACKTRACE_PARAMS, cause); - causeXContentBuilder.endObject(); - LifecycleExecutionState currentState = LifecycleExecutionState.fromIndexMetadata(idxMeta); - LifecycleExecutionState nextStepState = moveExecutionStateToNextStep(policyMetadata, currentState, currentStep, - new StepKey(currentStep.getPhase(), currentStep.getAction(), ErrorStep.NAME), nowSupplier, false); - LifecycleExecutionState.Builder failedState = LifecycleExecutionState.builder(nextStepState); - failedState.setFailedStep(currentStep.getName()); - failedState.setStepInfo(BytesReference.bytes(causeXContentBuilder).utf8ToString()); - Step failedStep = stepLookupFunction.apply(idxMeta, currentStep); - if (failedStep != null) { - // as an initial step we'll mark the failed step as auto retryable without actually looking at the cause to determine - // if the error is transient/recoverable from - failedState.setIsAutoRetryableError(failedStep.isRetryable()); - // maintain the retry count of the failed step as it will be cleared after a successful execution - failedState.setFailedStepRetryCount(currentState.getFailedStepRetryCount()); - } else { - logger.warn("failed step [{}] for index [{}] is not part of policy [{}] anymore, or it is invalid", - currentStep.getName(), index, policyMetadata.getName()); - } - - ClusterState.Builder newClusterStateBuilder = newClusterStateWithLifecycleState(index, clusterState, failedState.build()); - return newClusterStateBuilder.build(); - } - - ClusterState moveClusterStateToPreviouslyFailedStep(ClusterState currentState, String index, boolean isAutomaticRetry) { - ClusterState newState; - IndexMetaData indexMetaData = currentState.metaData().index(index); - if (indexMetaData == null) { - throw new IllegalArgumentException("index [" + index + "] does not exist"); - } - LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); - StepKey currentStepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); - String failedStep = lifecycleState.getFailedStep(); - if (currentStepKey != null && ErrorStep.NAME.equals(currentStepKey.getName()) && Strings.isNullOrEmpty(failedStep) == false) { - StepKey nextStepKey = new StepKey(currentStepKey.getPhase(), currentStepKey.getAction(), failedStep); - validateTransition(indexMetaData, currentStepKey, nextStepKey, stepRegistry); - IndexLifecycleMetadata ilmMeta = currentState.metaData().custom(IndexLifecycleMetadata.TYPE); - - LifecyclePolicyMetadata policyMetadata = ilmMeta.getPolicyMetadatas() - .get(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetaData.getSettings())); - LifecycleExecutionState nextStepState = moveExecutionStateToNextStep(policyMetadata, - lifecycleState, currentStepKey, nextStepKey, nowSupplier, true); - LifecycleExecutionState.Builder retryStepState = LifecycleExecutionState.builder(nextStepState); - retryStepState.setIsAutoRetryableError(lifecycleState.isAutoRetryableError()); - Integer currentRetryCount = lifecycleState.getFailedStepRetryCount(); - if (isAutomaticRetry) { - retryStepState.setFailedStepRetryCount(currentRetryCount == null ? 1 : ++currentRetryCount); - } else { - // manual retries don't update the retry count - retryStepState.setFailedStepRetryCount(lifecycleState.getFailedStepRetryCount()); - } - newState = newClusterStateWithLifecycleState(indexMetaData.getIndex(), currentState, retryStepState.build()).build(); - } else { - throw new IllegalArgumentException("cannot retry an action for an index [" - + index + "] that has not encountered an error when running a Lifecycle Policy"); - } - return newState; - } - - ClusterState moveClusterStateToPreviouslyFailedStep(ClusterState currentState, String[] indices) { - ClusterState newState = currentState; - for (String index : indices) { - newState = moveClusterStateToPreviouslyFailedStep(newState, index, false); - } - return newState; - } - - private static LifecycleExecutionState moveExecutionStateToNextStep(LifecyclePolicyMetadata policyMetadata, - LifecycleExecutionState existingState, - StepKey currentStep, StepKey nextStep, - LongSupplier nowSupplier, - boolean forcePhaseDefinitionRefresh) { - long nowAsMillis = nowSupplier.getAsLong(); - LifecycleExecutionState.Builder updatedState = LifecycleExecutionState.builder(existingState); - updatedState.setPhase(nextStep.getPhase()); - updatedState.setAction(nextStep.getAction()); - updatedState.setStep(nextStep.getName()); - updatedState.setStepTime(nowAsMillis); - - // clear any step info or error-related settings from the current step - updatedState.setFailedStep(null); - updatedState.setStepInfo(null); - updatedState.setIsAutoRetryableError(null); - updatedState.setFailedStepRetryCount(null); - - if (currentStep.getPhase().equals(nextStep.getPhase()) == false || forcePhaseDefinitionRefresh) { - final String newPhaseDefinition; - final Phase nextPhase; - if ("new".equals(nextStep.getPhase()) || TerminalPolicyStep.KEY.equals(nextStep)) { - nextPhase = null; - } else { - nextPhase = policyMetadata.getPolicy().getPhases().get(nextStep.getPhase()); - } - PhaseExecutionInfo phaseExecutionInfo = new PhaseExecutionInfo(policyMetadata.getName(), nextPhase, - policyMetadata.getVersion(), policyMetadata.getModifiedDate()); - newPhaseDefinition = Strings.toString(phaseExecutionInfo, false, false); - updatedState.setPhaseDefinition(newPhaseDefinition); - updatedState.setPhaseTime(nowAsMillis); - } else if (currentStep.getPhase().equals(InitializePolicyContextStep.INITIALIZATION_PHASE)) { - // The "new" phase is the initialization phase, usually the phase - // time would be set on phase transition, but since there is no - // transition into the "new" phase, we set it any time in the "new" - // phase - updatedState.setPhaseTime(nowAsMillis); - } - - if (currentStep.getAction().equals(nextStep.getAction()) == false) { - updatedState.setActionTime(nowAsMillis); - } - return updatedState.build(); - } - - static ClusterState.Builder newClusterStateWithLifecycleState(Index index, ClusterState clusterState, - LifecycleExecutionState lifecycleState) { - ClusterState.Builder newClusterStateBuilder = ClusterState.builder(clusterState); - newClusterStateBuilder.metaData(MetaData.builder(clusterState.getMetaData()) - .put(IndexMetaData.builder(clusterState.getMetaData().index(index)) - .putCustom(ILM_CUSTOM_METADATA_KEY, lifecycleState.asMap()))); - return newClusterStateBuilder; - } - - /** - * Conditionally updates cluster state with new step info. The new cluster state is only - * built if the step info has changed, otherwise the same old clusterState is - * returned - * - * @param index the index to modify - * @param clusterState the cluster state to modify - * @param stepInfo the new step info to update - * @return Updated cluster state with stepInfo if changed, otherwise the same cluster state - * if no changes to step info exist - * @throws IOException if parsing step info fails + * Move the index to the given {@code newStepKey}, always checks to ensure that the index's + * current step matches the {@code currentStepKey} prior to changing the state. */ - static ClusterState addStepInfoToClusterState(Index index, ClusterState clusterState, ToXContentObject stepInfo) throws IOException { - IndexMetaData indexMetaData = clusterState.getMetaData().index(index); - if (indexMetaData == null) { - // This index doesn't exist anymore, we can't do anything - return clusterState; - } - LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); - final String stepInfoString; - try (XContentBuilder infoXContentBuilder = JsonXContent.contentBuilder()) { - stepInfo.toXContent(infoXContentBuilder, ToXContent.EMPTY_PARAMS); - stepInfoString = BytesReference.bytes(infoXContentBuilder).utf8ToString(); - } - if (stepInfoString.equals(lifecycleState.getStepInfo())) { - return clusterState; - } - LifecycleExecutionState.Builder newState = LifecycleExecutionState.builder(lifecycleState); - newState.setStepInfo(stepInfoString); - ClusterState.Builder newClusterStateBuilder = newClusterStateWithLifecycleState(index, clusterState, newState.build()); - return newClusterStateBuilder.build(); - } - - private void moveToStep(Index index, String policy, StepKey currentStepKey, StepKey nextStepKey) { - logger.debug("[{}] moving to step [{}] {} -> {}", index.getName(), policy, currentStepKey, nextStepKey); + private void moveToStep(Index index, String policy, Step.StepKey currentStepKey, Step.StepKey newStepKey) { + logger.debug("[{}] moving to step [{}] {} -> {}", index.getName(), policy, currentStepKey, newStepKey); clusterService.submitStateUpdateTask("ilm-move-to-step", - new MoveToNextStepUpdateTask(index, policy, currentStepKey, nextStepKey, nowSupplier, clusterState -> + new MoveToNextStepUpdateTask(index, policy, currentStepKey, newStepKey, nowSupplier, stepRegistry, clusterState -> { IndexMetaData indexMetaData = clusterState.metaData().index(index); - if (nextStepKey != null && nextStepKey != TerminalPolicyStep.KEY && indexMetaData != null) { - maybeRunAsyncAction(clusterState, indexMetaData, policy, nextStepKey); + if (newStepKey != null && newStepKey != TerminalPolicyStep.KEY && indexMetaData != null) { + maybeRunAsyncAction(clusterState, indexMetaData, policy, newStepKey); } })); } - private void moveToErrorStep(Index index, String policy, StepKey currentStepKey, Exception e) { + /** + * Move the index to the ERROR step. + */ + private void moveToErrorStep(Index index, String policy, Step.StepKey currentStepKey, Exception e) { logger.error(new ParameterizedMessage("policy [{}] for index [{}] failed on step [{}]. Moving to ERROR step", - policy, index.getName(), currentStepKey), e); + policy, index.getName(), currentStepKey), e); clusterService.submitStateUpdateTask("ilm-move-to-error-step", new MoveToErrorStepUpdateTask(index, policy, currentStepKey, e, nowSupplier, stepRegistry::getStep)); } - private void setStepInfo(Index index, String policy, StepKey currentStepKey, ToXContentObject stepInfo) { + /** + * Set step info for the given index inside of its {@link LifecycleExecutionState} without + * changing other execution state. + */ + private void setStepInfo(Index index, String policy, Step.StepKey currentStepKey, ToXContentObject stepInfo) { clusterService.submitStateUpdateTask("ilm-set-step-info", new SetStepInfoUpdateTask(index, policy, currentStepKey, stepInfo)); } - public static ClusterState removePolicyForIndexes(final Index[] indices, ClusterState currentState, List failedIndexes) { - MetaData.Builder newMetadata = MetaData.builder(currentState.getMetaData()); - boolean clusterStateChanged = false; - for (Index index : indices) { - IndexMetaData indexMetadata = currentState.getMetaData().index(index); - if (indexMetadata == null) { - // Index doesn't exist so fail it - failedIndexes.add(index.getName()); - } else { - IndexMetaData.Builder newIdxMetadata = IndexLifecycleRunner.removePolicyForIndex(indexMetadata); - if (newIdxMetadata != null) { - newMetadata.put(newIdxMetadata); - clusterStateChanged = true; - } - } - } - if (clusterStateChanged) { - ClusterState.Builder newClusterState = ClusterState.builder(currentState); - newClusterState.metaData(newMetadata); - return newClusterState.build(); - } else { - return currentState; - } - } - - private static IndexMetaData.Builder removePolicyForIndex(IndexMetaData indexMetadata) { - Settings idxSettings = indexMetadata.getSettings(); - Settings.Builder newSettings = Settings.builder().put(idxSettings); - boolean notChanged = true; - - notChanged &= Strings.isNullOrEmpty(newSettings.remove(LifecycleSettings.LIFECYCLE_NAME_SETTING.getKey())); - notChanged &= Strings.isNullOrEmpty(newSettings.remove(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE_SETTING.getKey())); - notChanged &= Strings.isNullOrEmpty(newSettings.remove(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS_SETTING.getKey())); - long newSettingsVersion = notChanged ? indexMetadata.getSettingsVersion() : 1 + indexMetadata.getSettingsVersion(); - - IndexMetaData.Builder builder = IndexMetaData.builder(indexMetadata); - builder.removeCustom(ILM_CUSTOM_METADATA_KEY); - return builder.settings(newSettings).settingsVersion(newSettingsVersion); - } - + /** + * Mark the index with step info explaining that the policy doesn't exist. + */ private void markPolicyDoesNotExist(String policyName, Index index, LifecycleExecutionState executionState) { markPolicyRetrievalError(policyName, index, executionState, new IllegalArgumentException("policy [" + policyName + "] does not exist")); } + /** + * Mark the index with step info for a given error encountered while retrieving policy + * information. This is opposed to lifecycle execution errors, which would cause a transition to + * the ERROR step, however, the policy may be unparseable in which case there is no way to move + * to the ERROR step, so this is the best effort at capturing the error retrieving the policy. + */ private void markPolicyRetrievalError(String policyName, Index index, LifecycleExecutionState executionState, Exception e) { logger.debug( new ParameterizedMessage("unable to retrieve policy [{}] for index [{}], recording this in step_info for this index", - policyName, index.getName()), e); - setStepInfo(index, policyName, getCurrentStepKey(executionState), new SetStepInfoUpdateTask.ExceptionWrapper(e)); + policyName, index.getName()), e); + setStepInfo(index, policyName, LifecycleExecutionState.getCurrentStepKey(executionState), + new SetStepInfoUpdateTask.ExceptionWrapper(e)); } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java index 9deac0322e2a3..f116f9de08743 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java @@ -87,13 +87,33 @@ public void maybeRunAsyncAction(ClusterState clusterState, IndexMetaData indexMe lifecycleRunner.maybeRunAsyncAction(clusterState, indexMetaData, policyName, nextStepKey); } - public ClusterState moveClusterStateToStep(ClusterState currentState, String indexName, StepKey currentStepKey, StepKey nextStepKey) { - return IndexLifecycleRunner.moveClusterStateToStep(indexName, currentState, currentStepKey, nextStepKey, - nowSupplier, policyRegistry); + /** + * Move the cluster state to an arbitrary step for the provided index. + * + * In order to avoid a check-then-set race condition, the current step key + * is required in order to validate that the index is currently on the + * provided step. If it is not, an {@link IllegalArgumentException} is + * thrown. + * @throws IllegalArgumentException if the step movement cannot be validated + */ + public ClusterState moveClusterStateToStep(ClusterState currentState, Index index, StepKey currentStepKey, StepKey newStepKey) { + // We manually validate here, because any API must correctly specify the current step key + // when moving to an arbitrary step key (to avoid race conditions between the + // check-and-set). moveClusterStateToStep also does its own validation, but doesn't take + // the user-input for the current step (which is why we validate here for a passed in step) + IndexLifecycleTransition.validateTransition(currentState.getMetaData().index(index), + currentStepKey, newStepKey, policyRegistry); + return IndexLifecycleTransition.moveClusterStateToStep(index, currentState, newStepKey, + nowSupplier, policyRegistry, true); } public ClusterState moveClusterStateToPreviouslyFailedStep(ClusterState currentState, String[] indices) { - return lifecycleRunner.moveClusterStateToPreviouslyFailedStep(currentState, indices); + ClusterState newState = currentState; + for (String index : indices) { + newState = IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(newState, index, + nowSupplier, policyRegistry, false); + } + return newState; } @Override @@ -118,7 +138,7 @@ public void onMaster() { String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(idxMeta.getSettings()); if (Strings.isNullOrEmpty(policyName) == false) { final LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(idxMeta); - StepKey stepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey stepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); try { if (OperationMode.STOPPING == currentMode) { @@ -279,7 +299,7 @@ void triggerPolicies(ClusterState clusterState, boolean fromClusterStateChange) String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(idxMeta.getSettings()); if (Strings.isNullOrEmpty(policyName) == false) { final LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(idxMeta); - StepKey stepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey stepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); try { if (OperationMode.STOPPING == currentMode) { diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransition.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransition.java new file mode 100644 index 0000000000000..816186882927c --- /dev/null +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransition.java @@ -0,0 +1,342 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.ilm; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.support.TransportAction; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.index.Index; +import org.elasticsearch.xpack.core.ilm.ErrorStep; +import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; +import org.elasticsearch.xpack.core.ilm.InitializePolicyContextStep; +import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; +import org.elasticsearch.xpack.core.ilm.LifecycleSettings; +import org.elasticsearch.xpack.core.ilm.Phase; +import org.elasticsearch.xpack.core.ilm.PhaseExecutionInfo; +import org.elasticsearch.xpack.core.ilm.RolloverAction; +import org.elasticsearch.xpack.core.ilm.Step; +import org.elasticsearch.xpack.core.ilm.TerminalPolicyStep; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.LongSupplier; + +import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE; +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; + +/** + * The {@link IndexLifecycleTransition} class handles cluster state transitions + * related to ILM operations. These operations are all at the index level + * (inside of {@link IndexMetaData}) for the index in question. + * + * Each method is static and only changes a given state, no actions are + * performed by methods in this class. + */ +public final class IndexLifecycleTransition { + private static final Logger logger = LogManager.getLogger(IndexLifecycleTransition.class); + private static final ToXContent.Params STACKTRACE_PARAMS = + new ToXContent.MapParams(Collections.singletonMap(REST_EXCEPTION_SKIP_STACK_TRACE, "false")); + + /** + * Validates that the given transition from {@code currentStepKey} to {@code newStepKey} can be accomplished + * @throws IllegalArgumentException when the transition is not valid + */ + public static void validateTransition(IndexMetaData idxMeta, Step.StepKey currentStepKey, + Step.StepKey newStepKey, PolicyStepsRegistry stepRegistry) { + String indexName = idxMeta.getIndex().getName(); + Settings indexSettings = idxMeta.getSettings(); + String indexPolicySetting = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexSettings); + + // policy could be updated in-between execution + if (Strings.isNullOrEmpty(indexPolicySetting)) { + throw new IllegalArgumentException("index [" + indexName + "] is not associated with an Index Lifecycle Policy"); + } + + LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(idxMeta); + if (currentStepKey != null && currentStepKey.equals(LifecycleExecutionState.getCurrentStepKey(lifecycleState)) == false) { + throw new IllegalArgumentException("index [" + indexName + "] is not on current step [" + currentStepKey + "]"); + } + + if (stepRegistry.stepExists(indexPolicySetting, newStepKey) == false) { + throw new IllegalArgumentException("step [" + newStepKey + "] for index [" + idxMeta.getIndex().getName() + + "] with policy [" + indexPolicySetting + "] does not exist"); + } + } + + /** + * This method is intended for handling moving to different steps from {@link TransportAction} executions. + * For this reason, it is reasonable to throw {@link IllegalArgumentException} when state is not as expected. + * + * @param index The index whose step is to change + * @param state The current {@link ClusterState} + * @param newStepKey The new step to move the index into + * @param nowSupplier The current-time supplier for updating when steps changed + * @param stepRegistry The steps registry to check a step-key's existence in the index's current policy + * @param forcePhaseDefinitionRefresh Whether to force the phase JSON to be reread or not + * @return The updated cluster state where the index moved to newStepKey + */ + static ClusterState moveClusterStateToStep(Index index, ClusterState state, Step.StepKey newStepKey, LongSupplier nowSupplier, + PolicyStepsRegistry stepRegistry, boolean forcePhaseDefinitionRefresh) { + IndexMetaData idxMeta = state.getMetaData().index(index); + Step.StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(LifecycleExecutionState.fromIndexMetadata(idxMeta)); + validateTransition(idxMeta, currentStepKey, newStepKey, stepRegistry); + + Settings indexSettings = idxMeta.getSettings(); + String policy = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexSettings); + logger.info("moving index [{}] from [{}] to [{}] in policy [{}]", index.getName(), currentStepKey, newStepKey, policy); + + IndexLifecycleMetadata ilmMeta = state.metaData().custom(IndexLifecycleMetadata.TYPE); + LifecyclePolicyMetadata policyMetadata = ilmMeta.getPolicyMetadatas() + .get(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(idxMeta.getSettings())); + LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(idxMeta); + LifecycleExecutionState newLifecycleState = updateExecutionStateToStep(policyMetadata, + lifecycleState, newStepKey, nowSupplier, forcePhaseDefinitionRefresh); + ClusterState.Builder newClusterStateBuilder = newClusterStateWithLifecycleState(index, state, newLifecycleState); + + return newClusterStateBuilder.build(); + } + + /** + * Moves the given index into the ERROR step. The ERROR step will have the same phase and + * action, but use the {@link ErrorStep#NAME} as the name in the lifecycle execution state. + */ + static ClusterState moveClusterStateToErrorStep(Index index, ClusterState clusterState, Exception cause, LongSupplier nowSupplier, + BiFunction stepLookupFunction) throws IOException { + IndexMetaData idxMeta = clusterState.getMetaData().index(index); + IndexLifecycleMetadata ilmMeta = clusterState.metaData().custom(IndexLifecycleMetadata.TYPE); + LifecyclePolicyMetadata policyMetadata = ilmMeta.getPolicyMetadatas() + .get(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(idxMeta.getSettings())); + XContentBuilder causeXContentBuilder = JsonXContent.contentBuilder(); + causeXContentBuilder.startObject(); + ElasticsearchException.generateThrowableXContent(causeXContentBuilder, STACKTRACE_PARAMS, cause); + causeXContentBuilder.endObject(); + LifecycleExecutionState currentState = LifecycleExecutionState.fromIndexMetadata(idxMeta); + Step.StepKey currentStep = Objects.requireNonNull(LifecycleExecutionState.getCurrentStepKey(currentState), + "unable to move to an error step where there is no current step, state: " + currentState); + LifecycleExecutionState nextStepState = updateExecutionStateToStep(policyMetadata, currentState, + new Step.StepKey(currentStep.getPhase(), currentStep.getAction(), ErrorStep.NAME), nowSupplier, false); + + LifecycleExecutionState.Builder failedState = LifecycleExecutionState.builder(nextStepState); + failedState.setFailedStep(currentStep.getName()); + failedState.setStepInfo(BytesReference.bytes(causeXContentBuilder).utf8ToString()); + Step failedStep = stepLookupFunction.apply(idxMeta, currentStep); + + if (failedStep != null) { + // as an initial step we'll mark the failed step as auto retryable without actually looking at the cause to determine + // if the error is transient/recoverable from + failedState.setIsAutoRetryableError(failedStep.isRetryable()); + // maintain the retry count of the failed step as it will be cleared after a successful execution + failedState.setFailedStepRetryCount(currentState.getFailedStepRetryCount()); + } else { + logger.warn("failed step [{}] for index [{}] is not part of policy [{}] anymore, or it is invalid", + currentStep.getName(), index, policyMetadata.getName()); + } + + ClusterState.Builder newClusterStateBuilder = newClusterStateWithLifecycleState(index, clusterState, failedState.build()); + return newClusterStateBuilder.build(); + } + + /** + * Move the given index's execution state back to a step that had previously failed. If this is + * an automatic retry ({@code isAutomaticRetry}), the retry count is incremented. + */ + static ClusterState moveClusterStateToPreviouslyFailedStep(ClusterState currentState, String index, LongSupplier nowSupplier, + PolicyStepsRegistry stepRegistry, boolean isAutomaticRetry) { + ClusterState newState; + IndexMetaData indexMetaData = currentState.metaData().index(index); + if (indexMetaData == null) { + throw new IllegalArgumentException("index [" + index + "] does not exist"); + } + LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); + Step.StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); + String failedStep = lifecycleState.getFailedStep(); + if (currentStepKey != null && ErrorStep.NAME.equals(currentStepKey.getName()) && Strings.isNullOrEmpty(failedStep) == false) { + Step.StepKey nextStepKey = new Step.StepKey(currentStepKey.getPhase(), currentStepKey.getAction(), failedStep); + IndexLifecycleTransition.validateTransition(indexMetaData, currentStepKey, nextStepKey, stepRegistry); + IndexLifecycleMetadata ilmMeta = currentState.metaData().custom(IndexLifecycleMetadata.TYPE); + + LifecyclePolicyMetadata policyMetadata = ilmMeta.getPolicyMetadatas() + .get(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetaData.getSettings())); + LifecycleExecutionState nextStepState = IndexLifecycleTransition.updateExecutionStateToStep(policyMetadata, + lifecycleState, nextStepKey, nowSupplier, true); + LifecycleExecutionState.Builder retryStepState = LifecycleExecutionState.builder(nextStepState); + retryStepState.setIsAutoRetryableError(lifecycleState.isAutoRetryableError()); + Integer currentRetryCount = lifecycleState.getFailedStepRetryCount(); + if (isAutomaticRetry) { + retryStepState.setFailedStepRetryCount(currentRetryCount == null ? 1 : ++currentRetryCount); + } else { + // manual retries don't update the retry count + retryStepState.setFailedStepRetryCount(lifecycleState.getFailedStepRetryCount()); + } + newState = IndexLifecycleTransition.newClusterStateWithLifecycleState(indexMetaData.getIndex(), + currentState, retryStepState.build()).build(); + } else { + throw new IllegalArgumentException("cannot retry an action for an index [" + + index + "] that has not encountered an error when running a Lifecycle Policy"); + } + return newState; + } + + /** + * Given the existing execution state for an index, this updates pieces of the state with new + * timings and optionally the phase JSON (when transitioning to a different phase). + */ + private static LifecycleExecutionState updateExecutionStateToStep(LifecyclePolicyMetadata policyMetadata, + LifecycleExecutionState existingState, + Step.StepKey newStep, + LongSupplier nowSupplier, + boolean forcePhaseDefinitionRefresh) { + Step.StepKey currentStep = LifecycleExecutionState.getCurrentStepKey(existingState); + long nowAsMillis = nowSupplier.getAsLong(); + LifecycleExecutionState.Builder updatedState = LifecycleExecutionState.builder(existingState); + updatedState.setPhase(newStep.getPhase()); + updatedState.setAction(newStep.getAction()); + updatedState.setStep(newStep.getName()); + updatedState.setStepTime(nowAsMillis); + + // clear any step info or error-related settings from the current step + updatedState.setFailedStep(null); + updatedState.setStepInfo(null); + updatedState.setIsAutoRetryableError(null); + updatedState.setFailedStepRetryCount(null); + + if (currentStep == null || + currentStep.getPhase().equals(newStep.getPhase()) == false || + forcePhaseDefinitionRefresh) { + final String newPhaseDefinition; + final Phase nextPhase; + if ("new".equals(newStep.getPhase()) || TerminalPolicyStep.KEY.equals(newStep)) { + nextPhase = null; + } else { + nextPhase = policyMetadata.getPolicy().getPhases().get(newStep.getPhase()); + } + PhaseExecutionInfo phaseExecutionInfo = new PhaseExecutionInfo(policyMetadata.getName(), nextPhase, + policyMetadata.getVersion(), policyMetadata.getModifiedDate()); + newPhaseDefinition = Strings.toString(phaseExecutionInfo, false, false); + updatedState.setPhaseDefinition(newPhaseDefinition); + updatedState.setPhaseTime(nowAsMillis); + } else if (currentStep.getPhase().equals(InitializePolicyContextStep.INITIALIZATION_PHASE)) { + // The "new" phase is the initialization phase, usually the phase + // time would be set on phase transition, but since there is no + // transition into the "new" phase, we set it any time in the "new" + // phase + updatedState.setPhaseTime(nowAsMillis); + } + + if (currentStep == null || currentStep.getAction().equals(newStep.getAction()) == false) { + updatedState.setActionTime(nowAsMillis); + } + return updatedState.build(); + } + + /** + * Given a cluster state and lifecycle state, return a new state using the new lifecycle state for the given index. + */ + private static ClusterState.Builder newClusterStateWithLifecycleState(Index index, ClusterState clusterState, + LifecycleExecutionState lifecycleState) { + ClusterState.Builder newClusterStateBuilder = ClusterState.builder(clusterState); + newClusterStateBuilder.metaData(MetaData.builder(clusterState.getMetaData()) + .put(IndexMetaData.builder(clusterState.getMetaData().index(index)) + .putCustom(ILM_CUSTOM_METADATA_KEY, lifecycleState.asMap()))); + return newClusterStateBuilder; + } + + /** + * Conditionally updates cluster state with new step info. The new cluster state is only + * built if the step info has changed, otherwise the same old clusterState is + * returned + * + * @param index the index to modify + * @param clusterState the cluster state to modify + * @param stepInfo the new step info to update + * @return Updated cluster state with stepInfo if changed, otherwise the same cluster state + * if no changes to step info exist + * @throws IOException if parsing step info fails + */ + static ClusterState addStepInfoToClusterState(Index index, ClusterState clusterState, ToXContentObject stepInfo) throws IOException { + IndexMetaData indexMetaData = clusterState.getMetaData().index(index); + if (indexMetaData == null) { + // This index doesn't exist anymore, we can't do anything + return clusterState; + } + LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); + final String stepInfoString; + try (XContentBuilder infoXContentBuilder = JsonXContent.contentBuilder()) { + stepInfo.toXContent(infoXContentBuilder, ToXContent.EMPTY_PARAMS); + stepInfoString = BytesReference.bytes(infoXContentBuilder).utf8ToString(); + } + if (stepInfoString.equals(lifecycleState.getStepInfo())) { + return clusterState; + } + LifecycleExecutionState.Builder newState = LifecycleExecutionState.builder(lifecycleState); + newState.setStepInfo(stepInfoString); + ClusterState.Builder newClusterStateBuilder = newClusterStateWithLifecycleState(index, clusterState, newState.build()); + return newClusterStateBuilder.build(); + } + + /** + * Remove the ILM policy from the given indices, this removes the lifecycle setting as well as + * any lifecycle execution state that may be present in the index metadata + */ + public static ClusterState removePolicyForIndexes(final Index[] indices, ClusterState currentState, List failedIndexes) { + MetaData.Builder newMetadata = MetaData.builder(currentState.getMetaData()); + boolean clusterStateChanged = false; + for (Index index : indices) { + IndexMetaData indexMetadata = currentState.getMetaData().index(index); + if (indexMetadata == null) { + // Index doesn't exist so fail it + failedIndexes.add(index.getName()); + } else { + IndexMetaData.Builder newIdxMetadata = removePolicyForIndex(indexMetadata); + if (newIdxMetadata != null) { + newMetadata.put(newIdxMetadata); + clusterStateChanged = true; + } + } + } + if (clusterStateChanged) { + ClusterState.Builder newClusterState = ClusterState.builder(currentState); + newClusterState.metaData(newMetadata); + return newClusterState.build(); + } else { + return currentState; + } + } + + /** + * Remove ILM-related metadata from an index's {@link IndexMetaData} + */ + private static IndexMetaData.Builder removePolicyForIndex(IndexMetaData indexMetadata) { + Settings idxSettings = indexMetadata.getSettings(); + Settings.Builder newSettings = Settings.builder().put(idxSettings); + boolean notChanged = true; + + notChanged &= Strings.isNullOrEmpty(newSettings.remove(LifecycleSettings.LIFECYCLE_NAME_SETTING.getKey())); + notChanged &= Strings.isNullOrEmpty(newSettings.remove(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE_SETTING.getKey())); + notChanged &= Strings.isNullOrEmpty(newSettings.remove(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS_SETTING.getKey())); + long newSettingsVersion = notChanged ? indexMetadata.getSettingsVersion() : 1 + indexMetadata.getSettingsVersion(); + + IndexMetaData.Builder builder = IndexMetaData.builder(indexMetadata); + builder.removeCustom(ILM_CUSTOM_METADATA_KEY); + return builder.settings(newSettings).settingsVersion(newSettingsVersion); + } +} diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTask.java index 12a5714372521..1b80e070c5552 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTask.java @@ -63,9 +63,8 @@ public ClusterState execute(ClusterState currentState) throws IOException { Settings indexSettings = idxMeta.getSettings(); LifecycleExecutionState indexILMData = LifecycleExecutionState.fromIndexMetadata(idxMeta); if (policy.equals(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexSettings)) - && currentStepKey.equals(IndexLifecycleRunner.getCurrentStepKey(indexILMData))) { - return IndexLifecycleRunner.moveClusterStateToErrorStep(index, currentState, currentStepKey, cause, nowSupplier, - stepLookupFunction); + && currentStepKey.equals(LifecycleExecutionState.getCurrentStepKey(indexILMData))) { + return IndexLifecycleTransition.moveClusterStateToErrorStep(index, currentState, cause, nowSupplier, stepLookupFunction); } else { // either the policy has changed or the step is now // not the same as when we submitted the update task. In diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTask.java index c433a04002a5c..9fae96c2f5ff1 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTask.java @@ -28,15 +28,18 @@ public class MoveToNextStepUpdateTask extends ClusterStateUpdateTask { private final Step.StepKey currentStepKey; private final Step.StepKey nextStepKey; private final LongSupplier nowSupplier; + private final PolicyStepsRegistry stepRegistry; private final Consumer stateChangeConsumer; public MoveToNextStepUpdateTask(Index index, String policy, Step.StepKey currentStepKey, Step.StepKey nextStepKey, - LongSupplier nowSupplier, Consumer stateChangeConsumer) { + LongSupplier nowSupplier, PolicyStepsRegistry stepRegistry, + Consumer stateChangeConsumer) { this.index = index; this.policy = policy; this.currentStepKey = currentStepKey; this.nextStepKey = nextStepKey; this.nowSupplier = nowSupplier; + this.stepRegistry = stepRegistry; this.stateChangeConsumer = stateChangeConsumer; } @@ -66,9 +69,9 @@ public ClusterState execute(ClusterState currentState) { Settings indexSettings = indexMetaData.getSettings(); LifecycleExecutionState indexILMData = LifecycleExecutionState.fromIndexMetadata(currentState.getMetaData().index(index)); if (policy.equals(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexSettings)) - && currentStepKey.equals(IndexLifecycleRunner.getCurrentStepKey(indexILMData))) { + && currentStepKey.equals(LifecycleExecutionState.getCurrentStepKey(indexILMData))) { logger.trace("moving [{}] to next step ({})", index.getName(), nextStepKey); - return IndexLifecycleRunner.moveClusterStateToNextStep(index, currentState, currentStepKey, nextStepKey, nowSupplier, false); + return IndexLifecycleTransition.moveClusterStateToStep(index, currentState, nextStepKey, nowSupplier, stepRegistry, false); } else { // either the policy has changed or the step is now // not the same as when we submitted the update task. In diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/SetStepInfoUpdateTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/SetStepInfoUpdateTask.java index 2e1845ebe2c1a..20d11a098e8b9 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/SetStepInfoUpdateTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/SetStepInfoUpdateTask.java @@ -60,8 +60,8 @@ public ClusterState execute(ClusterState currentState) throws IOException { Settings indexSettings = idxMeta.getSettings(); LifecycleExecutionState indexILMData = LifecycleExecutionState.fromIndexMetadata(idxMeta); if (policy.equals(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexSettings)) - && Objects.equals(currentStepKey, IndexLifecycleRunner.getCurrentStepKey(indexILMData))) { - return IndexLifecycleRunner.addStepInfoToClusterState(index, currentState, stepInfo); + && Objects.equals(currentStepKey, LifecycleExecutionState.getCurrentStepKey(indexILMData))) { + return IndexLifecycleTransition.addStepInfoToClusterState(index, currentState, stepInfo); } else { // either the policy has changed or the step is now // not the same as when we submitted the update task. In diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMoveToStepAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMoveToStepAction.java index 9958fba2b90d0..9e0e68cd1e267 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMoveToStepAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMoveToStepAction.java @@ -64,7 +64,7 @@ protected void masterOperation(Task task, Request request, ClusterState state, A new AckedClusterStateUpdateTask(request, listener) { @Override public ClusterState execute(ClusterState currentState) { - return indexLifecycleService.moveClusterStateToStep(currentState, request.getIndex(), request.getCurrentStepKey(), + return indexLifecycleService.moveClusterStateToStep(currentState, indexMetaData.getIndex(), request.getCurrentStepKey(), request.getNextStepKey()); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportRemoveIndexLifecyclePolicyAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportRemoveIndexLifecyclePolicyAction.java index 335710660ed6f..45be96b5528e6 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportRemoveIndexLifecyclePolicyAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportRemoveIndexLifecyclePolicyAction.java @@ -24,7 +24,7 @@ import org.elasticsearch.xpack.core.ilm.action.RemoveIndexLifecyclePolicyAction; import org.elasticsearch.xpack.core.ilm.action.RemoveIndexLifecyclePolicyAction.Request; import org.elasticsearch.xpack.core.ilm.action.RemoveIndexLifecyclePolicyAction.Response; -import org.elasticsearch.xpack.ilm.IndexLifecycleRunner; +import org.elasticsearch.xpack.ilm.IndexLifecycleTransition; import java.io.IOException; import java.util.ArrayList; @@ -65,7 +65,7 @@ protected void masterOperation(Task task, Request request, ClusterState state, A @Override public ClusterState execute(ClusterState currentState) throws Exception { - return IndexLifecycleRunner.removePolicyForIndexes(indices, currentState, failedIndexes); + return IndexLifecycleTransition.removePolicyForIndexes(indices, currentState, failedIndexes); } @Override diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java index 9a22bc59fda75..8678c0d1a13cc 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java @@ -158,7 +158,7 @@ public void testSuccessThenFailureUnsetNextKey() throws IOException { ExecuteStepsUpdateTask task = new ExecuteStepsUpdateTask(mixedPolicyName, index, startStep, policyStepsRegistry, null, () -> now); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); - StepKey currentStepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); assertThat(currentStepKey, equalTo(secondStepKey)); assertThat(firstStep.getExecuteCount(), equalTo(1L)); assertThat(secondStep.getExecuteCount(), equalTo(1L)); @@ -175,7 +175,7 @@ public void testExecuteUntilFirstNonClusterStateStep() throws IOException { ExecuteStepsUpdateTask task = new ExecuteStepsUpdateTask(mixedPolicyName, index, startStep, policyStepsRegistry, null, () -> now); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); - StepKey currentStepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); assertThat(currentStepKey, equalTo(thirdStepKey)); assertThat(firstStep.getExecuteCount(), equalTo(0L)); assertThat(secondStep.getExecuteCount(), equalTo(1L)); @@ -214,7 +214,7 @@ public void testExecuteIncompleteWaitStepNoInfo() throws IOException { ExecuteStepsUpdateTask task = new ExecuteStepsUpdateTask(mixedPolicyName, index, startStep, policyStepsRegistry, null, () -> now); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); - StepKey currentStepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); assertThat(currentStepKey, equalTo(secondStepKey)); assertThat(firstStep.getExecuteCount(), equalTo(0L)); assertThat(secondStep.getExecuteCount(), equalTo(1L)); @@ -233,7 +233,7 @@ public void testExecuteIncompleteWaitStepWithInfo() throws IOException { ExecuteStepsUpdateTask task = new ExecuteStepsUpdateTask(mixedPolicyName, index, startStep, policyStepsRegistry, null, () -> now); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); - StepKey currentStepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); assertThat(currentStepKey, equalTo(secondStepKey)); assertThat(firstStep.getExecuteCount(), equalTo(0L)); assertThat(secondStep.getExecuteCount(), equalTo(1L)); @@ -264,7 +264,7 @@ public void testClusterActionStepThrowsException() throws IOException { ExecuteStepsUpdateTask task = new ExecuteStepsUpdateTask(mixedPolicyName, index, startStep, policyStepsRegistry, null, () -> now); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); - StepKey currentStepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); assertThat(currentStepKey, equalTo(new StepKey(firstStepKey.getPhase(), firstStepKey.getAction(), ErrorStep.NAME))); assertThat(firstStep.getExecuteCount(), equalTo(1L)); assertThat(secondStep.getExecuteCount(), equalTo(0L)); @@ -284,7 +284,7 @@ public void testClusterWaitStepThrowsException() throws IOException { ExecuteStepsUpdateTask task = new ExecuteStepsUpdateTask(mixedPolicyName, index, startStep, policyStepsRegistry, null, () -> now); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); - StepKey currentStepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); assertThat(currentStepKey, equalTo(new StepKey(firstStepKey.getPhase(), firstStepKey.getAction(), ErrorStep.NAME))); assertThat(firstStep.getExecuteCount(), equalTo(1L)); assertThat(secondStep.getExecuteCount(), equalTo(1L)); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java index 20d1a0aed726a..2580e2970e521 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java @@ -5,7 +5,8 @@ */ package org.elasticsearch.xpack.ilm; -import org.elasticsearch.ElasticsearchException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.Version; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterName; @@ -19,7 +20,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.settings.Settings.Builder; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ToXContent; @@ -32,7 +32,6 @@ import org.elasticsearch.test.client.NoOpClient; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.xpack.core.ilm.AbstractStepTestCase; import org.elasticsearch.xpack.core.ilm.AsyncActionStep; import org.elasticsearch.xpack.core.ilm.AsyncWaitStep; import org.elasticsearch.xpack.core.ilm.ClusterStateActionStep; @@ -63,6 +62,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -75,8 +75,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.stream.Collectors; import static java.util.stream.Collectors.toList; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.awaitLatch; @@ -85,7 +83,6 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -112,55 +109,6 @@ public void shutdown() { threadPool.shutdownNow(); } - /** A real policy steps registry where getStep can be overridden so that JSON doesn't have to be parsed */ - private class MockPolicyStepsRegistry extends PolicyStepsRegistry { - private BiFunction fn = null; - - MockPolicyStepsRegistry(SortedMap lifecyclePolicyMap, Map firstStepMap, - Map> stepMap, NamedXContentRegistry xContentRegistry, Client client) { - super(lifecyclePolicyMap, firstStepMap, stepMap, xContentRegistry, client); - } - - public void setResolver(BiFunction fn) { - this.fn = fn; - } - - @Override - public Step getStep(IndexMetaData indexMetaData, StepKey stepKey) { - if (fn == null) { - logger.info("--> retrieving step {}", stepKey); - return super.getStep(indexMetaData, stepKey); - } else { - logger.info("--> returning mock step"); - return fn.apply(indexMetaData, stepKey); - } - } - } - - private MockPolicyStepsRegistry createOneStepPolicyStepRegistry(String policyName, Step step) { - return createOneStepPolicyStepRegistry(policyName, step, "test"); - } - - private MockPolicyStepsRegistry createOneStepPolicyStepRegistry(String policyName, Step step, String indexName) { - LifecyclePolicy policy = new LifecyclePolicy(policyName, new HashMap<>()); - SortedMap lifecyclePolicyMap = new TreeMap<>(); - lifecyclePolicyMap.put(policyName, new LifecyclePolicyMetadata(policy, new HashMap<>(), 1, 1)); - Map firstStepMap = new HashMap<>(); - firstStepMap.put(policyName, step); - Map> stepMap = new HashMap<>(); - Map policySteps = new HashMap<>(); - policySteps.put(step.getKey(), step); - stepMap.put(policyName, policySteps); - Map> indexSteps = new HashMap<>(); - List steps = new ArrayList<>(); - steps.add(step); - Index index = new Index(indexName, indexName + "uuid"); - indexSteps.put(index, steps); - Client client = mock(Client.class); - when(client.settings()).thenReturn(Settings.EMPTY); - return new MockPolicyStepsRegistry(lifecyclePolicyMap, firstStepMap, stepMap, REGISTRY, client); - } - public void testRunPolicyTerminalPolicyStep() { String policyName = "async_action_policy"; TerminalPolicyStep step = TerminalPolicyStep.INSTANCE; @@ -295,7 +243,7 @@ public void testRunStateChangePolicyWithNextStep() throws Exception { StepKey nextStepKey = new StepKey("phase", "action", "next_cluster_state_action_step"); MockClusterStateActionStep step = new MockClusterStateActionStep(stepKey, nextStepKey); MockClusterStateActionStep nextStep = new MockClusterStateActionStep(nextStepKey, null); - MockPolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); + MockPolicyStepsRegistry stepRegistry = createMultiStepPolicyStepRegistry(policyName, Arrays.asList(step, nextStep)); stepRegistry.setResolver((i, k) -> { if (stepKey.equals(k)) { return step; @@ -481,7 +429,7 @@ public void testRunStateChangePolicyWithAsyncActionNextStep() throws Exception { StepKey nextStepKey = new StepKey("phase", "action", "async_action_step"); MockClusterStateActionStep step = new MockClusterStateActionStep(stepKey, nextStepKey); MockAsyncActionStep nextStep = new MockAsyncActionStep(nextStepKey, null); - MockPolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); + MockPolicyStepsRegistry stepRegistry = createMultiStepPolicyStepRegistry(policyName, Arrays.asList(step, nextStep)); stepRegistry.setResolver((i, k) -> { if (stepKey.equals(k)) { return step; @@ -696,65 +644,6 @@ public void testRunPolicyThatDoesntExist() { Mockito.verifyNoMoreInteractions(clusterService); } - public void testGetCurrentStepKey() { - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - StepKey stepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState.build()); - assertNull(stepKey); - - String phase = randomAlphaOfLength(20); - String action = randomAlphaOfLength(20); - String step = randomAlphaOfLength(20); - LifecycleExecutionState.Builder lifecycleState2 = LifecycleExecutionState.builder(); - lifecycleState2.setPhase(phase); - lifecycleState2.setAction(action); - lifecycleState2.setStep(step); - stepKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState2.build()); - assertNotNull(stepKey); - assertEquals(phase, stepKey.getPhase()); - assertEquals(action, stepKey.getAction()); - assertEquals(step, stepKey.getName()); - - phase = randomAlphaOfLength(20); - action = randomAlphaOfLength(20); - step = null; - LifecycleExecutionState.Builder lifecycleState3 = LifecycleExecutionState.builder(); - lifecycleState3.setPhase(phase); - lifecycleState3.setAction(action); - lifecycleState3.setStep(step); - AssertionError error3 = expectThrows(AssertionError.class, () -> IndexLifecycleRunner.getCurrentStepKey(lifecycleState3.build())); - assertEquals("Current phase is not empty: " + phase, error3.getMessage()); - - phase = null; - action = randomAlphaOfLength(20); - step = null; - LifecycleExecutionState.Builder lifecycleState4 = LifecycleExecutionState.builder(); - lifecycleState4.setPhase(phase); - lifecycleState4.setAction(action); - lifecycleState4.setStep(step); - AssertionError error4 = expectThrows(AssertionError.class, () -> IndexLifecycleRunner.getCurrentStepKey(lifecycleState4.build())); - assertEquals("Current action is not empty: " + action, error4.getMessage()); - - phase = null; - action = randomAlphaOfLength(20); - step = randomAlphaOfLength(20); - LifecycleExecutionState.Builder lifecycleState5 = LifecycleExecutionState.builder(); - lifecycleState5.setPhase(phase); - lifecycleState5.setAction(action); - lifecycleState5.setStep(step); - AssertionError error5 = expectThrows(AssertionError.class, () -> IndexLifecycleRunner.getCurrentStepKey(lifecycleState5.build())); - assertEquals(null, error5.getMessage()); - - phase = null; - action = null; - step = randomAlphaOfLength(20); - LifecycleExecutionState.Builder lifecycleState6 = LifecycleExecutionState.builder(); - lifecycleState6.setPhase(phase); - lifecycleState6.setAction(action); - lifecycleState6.setStep(step); - AssertionError error6 = expectThrows(AssertionError.class, () -> IndexLifecycleRunner.getCurrentStepKey(lifecycleState6.build())); - assertEquals(null, error6.getMessage()); - } - public void testGetCurrentStep() { String policyName = "policy"; StepKey firstStepKey = new StepKey("phase_1", "action_1", "step_1"); @@ -796,561 +685,18 @@ public void testGetCurrentStep() { PolicyStepsRegistry registry = new PolicyStepsRegistry(metas, firstStepMap, stepMap, REGISTRY, client); // First step is retrieved because there are no settings for the index - Step stepFromNoSettings = IndexLifecycleRunner.getCurrentStep(registry, policy.getName(), indexMetaData, - LifecycleExecutionState.builder().build()); + IndexMetaData indexMetaDataWithNoKey = IndexMetaData.builder(index.getName()) + .settings(indexSettings) + .putCustom(ILM_CUSTOM_METADATA_KEY, LifecycleExecutionState.builder().build().asMap()) + .build(); + Step stepFromNoSettings = IndexLifecycleRunner.getCurrentStep(registry, policy.getName(), indexMetaDataWithNoKey); assertEquals(firstStep, stepFromNoSettings); // The step that was written into the metadata is retrieved - Step currentStep = IndexLifecycleRunner.getCurrentStep(registry, policy.getName(), indexMetaData, lifecycleState.build()); + Step currentStep = IndexLifecycleRunner.getCurrentStep(registry, policy.getName(), indexMetaData); assertEquals(step.getKey(), currentStep.getKey()); } - public void testMoveClusterStateToNextStep() { - String indexName = "my_index"; - LifecyclePolicy policy = randomValueOtherThanMany(p -> p.getPhases().size() == 0, - () -> LifecyclePolicyTests.randomTestLifecyclePolicy("policy")); - Phase nextPhase = policy.getPhases().values().stream().findFirst().get(); - List policyMetadatas = Collections.singletonList( - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong())); - StepKey currentStep = new StepKey("current_phase", "current_action", "current_step"); - StepKey nextStep = new StepKey(nextPhase.getName(), "next_action", "next_step"); - long now = randomNonNegativeLong(); - - // test going from null lifecycle settings to next step - ClusterState clusterState = buildClusterState(indexName, - Settings.builder() - .put(LifecycleSettings.LIFECYCLE_NAME, policy.getName()), LifecycleExecutionState.builder().build(), policyMetadatas); - Index index = clusterState.metaData().index(indexName).getIndex(); - ClusterState newClusterState = IndexLifecycleRunner.moveClusterStateToNextStep(index, clusterState, currentStep, nextStep, - () -> now, false); - assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); - - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStep.getPhase()); - lifecycleState.setAction(currentStep.getAction()); - lifecycleState.setStep(currentStep.getName()); - // test going from set currentStep settings to nextStep - Builder indexSettingsBuilder = Settings.builder() - .put(LifecycleSettings.LIFECYCLE_NAME, policy.getName()); - if (randomBoolean()) { - lifecycleState.setStepInfo(randomAlphaOfLength(20)); - } - - clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); - index = clusterState.metaData().index(indexName).getIndex(); - newClusterState = IndexLifecycleRunner.moveClusterStateToNextStep(index, clusterState, currentStep, nextStep, () -> now, false); - assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); - } - - public void testMoveClusterStateToNextStepSamePhase() { - String indexName = "my_index"; - StepKey currentStep = new StepKey("current_phase", "current_action", "current_step"); - StepKey nextStep = new StepKey("current_phase", "next_action", "next_step"); - long now = randomNonNegativeLong(); - - ClusterState clusterState = buildClusterState(indexName, Settings.builder(), LifecycleExecutionState.builder().build(), - Collections.emptyList()); - Index index = clusterState.metaData().index(indexName).getIndex(); - ClusterState newClusterState = IndexLifecycleRunner.moveClusterStateToNextStep(index, clusterState, currentStep, nextStep, - () -> now, false); - assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); - - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStep.getPhase()); - lifecycleState.setAction(currentStep.getAction()); - lifecycleState.setStep(currentStep.getName()); - if (randomBoolean()) { - lifecycleState.setStepInfo(randomAlphaOfLength(20)); - } - - clusterState = buildClusterState(indexName, Settings.builder(), lifecycleState.build(), Collections.emptyList()); - index = clusterState.metaData().index(indexName).getIndex(); - newClusterState = IndexLifecycleRunner.moveClusterStateToNextStep(index, clusterState, currentStep, nextStep, () -> now, false); - assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); - } - - public void testMoveClusterStateToNextStepSameAction() { - String indexName = "my_index"; - StepKey currentStep = new StepKey("current_phase", "current_action", "current_step"); - StepKey nextStep = new StepKey("current_phase", "current_action", "next_step"); - long now = randomNonNegativeLong(); - - ClusterState clusterState = buildClusterState(indexName, Settings.builder(), LifecycleExecutionState.builder().build(), - Collections.emptyList()); - Index index = clusterState.metaData().index(indexName).getIndex(); - ClusterState newClusterState = IndexLifecycleRunner.moveClusterStateToNextStep(index, clusterState, currentStep, nextStep, - () -> now, false); - assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); - - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStep.getPhase()); - lifecycleState.setAction(currentStep.getAction()); - lifecycleState.setStep(currentStep.getName()); - if (randomBoolean()) { - lifecycleState.setStepInfo(randomAlphaOfLength(20)); - } - clusterState = buildClusterState(indexName, Settings.builder(), lifecycleState.build(), Collections.emptyList()); - index = clusterState.metaData().index(indexName).getIndex(); - newClusterState = IndexLifecycleRunner.moveClusterStateToNextStep(index, clusterState, currentStep, nextStep, () -> now, false); - assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); - } - - public void testSuccessfulValidatedMoveClusterStateToNextStep() { - String indexName = "my_index"; - String policyName = "my_policy"; - LifecyclePolicy policy = randomValueOtherThanMany(p -> p.getPhases().size() == 0, - () -> LifecyclePolicyTests.randomTestLifecyclePolicy(policyName)); - Phase nextPhase = policy.getPhases().values().stream().findFirst().get(); - List policyMetadatas = Collections.singletonList( - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong())); - StepKey currentStepKey = new StepKey("current_phase", "current_action", "current_step"); - StepKey nextStepKey = new StepKey(nextPhase.getName(), "next_action", "next_step"); - long now = randomNonNegativeLong(); - Step step = new MockStep(nextStepKey, nextStepKey); - PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step, indexName); - - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStepKey.getPhase()); - lifecycleState.setAction(currentStepKey.getAction()); - lifecycleState.setStep(currentStepKey.getName()); - - Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, policyName); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); - Index index = clusterState.metaData().index(indexName).getIndex(); - ClusterState newClusterState = IndexLifecycleRunner.moveClusterStateToStep(indexName, clusterState, currentStepKey, - nextStepKey, () -> now, stepRegistry); - assertClusterStateOnNextStep(clusterState, index, currentStepKey, nextStepKey, newClusterState, now); - } - - public void testValidatedMoveClusterStateToNextStepWithoutPolicy() { - String indexName = "my_index"; - String policyName = "policy"; - StepKey currentStepKey = new StepKey("current_phase", "current_action", "current_step"); - StepKey nextStepKey = new StepKey("next_phase", "next_action", "next_step"); - long now = randomNonNegativeLong(); - Step step = new MockStep(nextStepKey, nextStepKey); - PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); - - Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, randomBoolean() ? "" : null); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStepKey.getPhase()); - lifecycleState.setAction(currentStepKey.getAction()); - lifecycleState.setStep(currentStepKey.getName()); - - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); - IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, - () -> IndexLifecycleRunner.moveClusterStateToStep(indexName, clusterState, currentStepKey, - nextStepKey, () -> now, stepRegistry)); - assertThat(exception.getMessage(), equalTo("index [my_index] is not associated with an Index Lifecycle Policy")); - } - - public void testValidatedMoveClusterStateToNextStepInvalidCurrentStep() { - String indexName = "my_index"; - String policyName = "my_policy"; - StepKey currentStepKey = new StepKey("current_phase", "current_action", "current_step"); - StepKey notCurrentStepKey = new StepKey("not_current_phase", "not_current_action", "not_current_step"); - StepKey nextStepKey = new StepKey("next_phase", "next_action", "next_step"); - long now = randomNonNegativeLong(); - Step step = new MockStep(nextStepKey, nextStepKey); - PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); - - Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, policyName); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStepKey.getPhase()); - lifecycleState.setAction(currentStepKey.getAction()); - lifecycleState.setStep(currentStepKey.getName()); - - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); - IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, - () -> IndexLifecycleRunner.moveClusterStateToStep(indexName, clusterState, notCurrentStepKey, - nextStepKey, () -> now, stepRegistry)); - assertThat(exception.getMessage(), equalTo("index [my_index] is not on current step " + - "[{\"phase\":\"not_current_phase\",\"action\":\"not_current_action\",\"name\":\"not_current_step\"}]")); - } - - public void testValidatedMoveClusterStateToNextStepInvalidNextStep() { - String indexName = "my_index"; - String policyName = "my_policy"; - StepKey currentStepKey = new StepKey("current_phase", "current_action", "current_step"); - StepKey nextStepKey = new StepKey("next_phase", "next_action", "next_step"); - long now = randomNonNegativeLong(); - Step step = new MockStep(currentStepKey, nextStepKey); - PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); - - Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, policyName); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStepKey.getPhase()); - lifecycleState.setAction(currentStepKey.getAction()); - lifecycleState.setStep(currentStepKey.getName()); - - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); - IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, - () -> IndexLifecycleRunner.moveClusterStateToStep(indexName, clusterState, currentStepKey, - nextStepKey, () -> now, stepRegistry)); - assertThat(exception.getMessage(), - equalTo("step [{\"phase\":\"next_phase\",\"action\":\"next_action\",\"name\":\"next_step\"}] " + - "for index [my_index] with policy [my_policy] does not exist")); - } - - public void testMoveClusterStateToErrorStep() throws IOException { - String indexName = "my_index"; - StepKey currentStep = new StepKey("current_phase", "current_action", "current_step"); - StepKey nextStepKey = new StepKey("next_phase", "next_action", "next_step"); - long now = randomNonNegativeLong(); - Exception cause = new ElasticsearchException("THIS IS AN EXPECTED CAUSE"); - - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStep.getPhase()); - lifecycleState.setAction(currentStep.getAction()); - lifecycleState.setStep(currentStep.getName()); - ClusterState clusterState = buildClusterState(indexName, Settings.builder(), lifecycleState.build(), Collections.emptyList()); - Index index = clusterState.metaData().index(indexName).getIndex(); - - ClusterState newClusterState = IndexLifecycleRunner.moveClusterStateToErrorStep(index, clusterState, currentStep, cause, () -> now, - (idxMeta, stepKey) -> new MockStep(stepKey, nextStepKey)); - assertClusterStateOnErrorStep(clusterState, index, currentStep, newClusterState, now, - "{\"type\":\"exception\",\"reason\":\"THIS IS AN EXPECTED CAUSE\""); - - cause = new IllegalArgumentException("non elasticsearch-exception"); - newClusterState = IndexLifecycleRunner.moveClusterStateToErrorStep(index, clusterState, currentStep, cause, () -> now, - (idxMeta, stepKey) -> new MockStep(stepKey, nextStepKey)); - assertClusterStateOnErrorStep(clusterState, index, currentStep, newClusterState, now, - "{\"type\":\"illegal_argument_exception\",\"reason\":\"non elasticsearch-exception\",\"stack_trace\":\""); - } - - public void testMoveClusterStateToFailedStep() { - String indexName = "my_index"; - String[] indices = new String[] { indexName }; - String policyName = "my_policy"; - long now = randomNonNegativeLong(); - StepKey failedStepKey = new StepKey("current_phase", MockAction.NAME, "current_step"); - StepKey errorStepKey = new StepKey(failedStepKey.getPhase(), failedStepKey.getAction(), ErrorStep.NAME); - Step step = new MockStep(failedStepKey, null); - LifecyclePolicy policy = createPolicy(policyName, failedStepKey, null); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Collections.emptyMap(), - randomNonNegativeLong(), randomNonNegativeLong()); - - PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policyName, step, indexName); - Settings.Builder indexSettingsBuilder = Settings.builder() - .put(LifecycleSettings.LIFECYCLE_NAME, policyName); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(errorStepKey.getPhase()); - lifecycleState.setPhaseTime(now); - lifecycleState.setAction(errorStepKey.getAction()); - lifecycleState.setActionTime(now); - lifecycleState.setStep(errorStepKey.getName()); - lifecycleState.setStepTime(now); - lifecycleState.setFailedStep(failedStepKey.getName()); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), - Collections.singletonList(policyMetadata)); - Index index = clusterState.metaData().index(indexName).getIndex(); - IndexLifecycleRunner runner = new IndexLifecycleRunner(policyRegistry, null, threadPool, () -> now); - ClusterState nextClusterState = runner.moveClusterStateToPreviouslyFailedStep(clusterState, indices); - IndexLifecycleRunnerTests.assertClusterStateOnNextStep(clusterState, index, errorStepKey, failedStepKey, - nextClusterState, now); - LifecycleExecutionState executionState = LifecycleExecutionState.fromIndexMetadata(nextClusterState.metaData().index(indexName)); - assertThat("manual move to failed step should not count as a retry", executionState.getFailedStepRetryCount(), is(nullValue())); - } - - public void testMoveClusterStateToFailedStepWithUnknownStep() { - String indexName = "my_index"; - String[] indices = new String[] { indexName }; - String policyName = "my_policy"; - long now = randomNonNegativeLong(); - StepKey failedStepKey = new StepKey("current_phase", MockAction.NAME, "current_step"); - StepKey errorStepKey = new StepKey(failedStepKey.getPhase(), failedStepKey.getAction(), ErrorStep.NAME); - - StepKey registeredStepKey = new StepKey(randomFrom(failedStepKey.getPhase(), "other"), - MockAction.NAME, "different_step"); - Step step = new MockStep(registeredStepKey, null); - LifecyclePolicy policy = createPolicy(policyName, failedStepKey, null); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Collections.emptyMap(), - randomNonNegativeLong(), randomNonNegativeLong()); - - PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policyName, step, indexName); - Settings.Builder indexSettingsBuilder = Settings.builder() - .put(LifecycleSettings.LIFECYCLE_NAME, policyName); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(errorStepKey.getPhase()); - lifecycleState.setPhaseTime(now); - lifecycleState.setAction(errorStepKey.getAction()); - lifecycleState.setActionTime(now); - lifecycleState.setStep(errorStepKey.getName()); - lifecycleState.setStepTime(now); - lifecycleState.setFailedStep(failedStepKey.getName()); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), - Collections.singletonList(policyMetadata)); - IndexLifecycleRunner runner = new IndexLifecycleRunner(policyRegistry, null, threadPool, () -> now); - IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, - () -> runner.moveClusterStateToPreviouslyFailedStep(clusterState, indices)); - assertThat(exception.getMessage(), equalTo("step [" + failedStepKey - + "] for index [my_index] with policy [my_policy] does not exist")); - } - - public void testMoveClusterStateToFailedStepIndexNotFound() { - String existingIndexName = "my_index"; - String invalidIndexName = "does_not_exist"; - ClusterState clusterState = buildClusterState(existingIndexName, Settings.builder(), LifecycleExecutionState.builder().build(), - Collections.emptyList()); - IndexLifecycleRunner runner = new IndexLifecycleRunner(null, null, threadPool, () -> 0L); - IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, - () -> runner.moveClusterStateToPreviouslyFailedStep(clusterState, new String[] { invalidIndexName })); - assertThat(exception.getMessage(), equalTo("index [" + invalidIndexName + "] does not exist")); - } - - public void testMoveClusterStateToFailedStepInvalidPolicySetting() { - String indexName = "my_index"; - String[] indices = new String[] { indexName }; - String policyName = "my_policy"; - long now = randomNonNegativeLong(); - StepKey failedStepKey = new StepKey("current_phase", "current_action", "current_step"); - StepKey errorStepKey = new StepKey(failedStepKey.getPhase(), failedStepKey.getAction(), ErrorStep.NAME); - Step step = new MockStep(failedStepKey, null); - PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policyName, step); - Settings.Builder indexSettingsBuilder = Settings.builder() - .put(LifecycleSettings.LIFECYCLE_NAME, (String) null); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(errorStepKey.getPhase()); - lifecycleState.setAction(errorStepKey.getAction()); - lifecycleState.setStep(errorStepKey.getName()); - lifecycleState.setFailedStep(failedStepKey.getName()); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); - IndexLifecycleRunner runner = new IndexLifecycleRunner(policyRegistry, null, threadPool, () -> now); - IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, - () -> runner.moveClusterStateToPreviouslyFailedStep(clusterState, indices)); - assertThat(exception.getMessage(), equalTo("index [" + indexName + "] is not associated with an Index Lifecycle Policy")); - } - - public void testMoveClusterStateToFailedNotOnError() { - String indexName = "my_index"; - String[] indices = new String[] { indexName }; - String policyName = "my_policy"; - long now = randomNonNegativeLong(); - StepKey failedStepKey = new StepKey("current_phase", "current_action", "current_step"); - Step step = new MockStep(failedStepKey, null); - PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policyName, step); - Settings.Builder indexSettingsBuilder = Settings.builder() - .put(LifecycleSettings.LIFECYCLE_NAME, (String) null); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(failedStepKey.getPhase()); - lifecycleState.setAction(failedStepKey.getAction()); - lifecycleState.setStep(failedStepKey.getName()); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); - IndexLifecycleRunner runner = new IndexLifecycleRunner(policyRegistry, null, threadPool, () -> now); - IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, - () -> runner.moveClusterStateToPreviouslyFailedStep(clusterState, indices)); - assertThat(exception.getMessage(), equalTo("cannot retry an action for an index [" + indices[0] - + "] that has not encountered an error when running a Lifecycle Policy")); - } - - public void testMoveClusterStateToPreviouslyFailedStepAsAutomaticRetry() { - String indexName = "my_index"; - String policyName = "my_policy"; - long now = randomNonNegativeLong(); - StepKey failedStepKey = new StepKey("current_phase", MockAction.NAME, "current_step"); - StepKey errorStepKey = new StepKey(failedStepKey.getPhase(), failedStepKey.getAction(), ErrorStep.NAME); - Step retryableStep = new RetryableMockStep(failedStepKey, null); - LifecyclePolicy policy = createPolicy(policyName, failedStepKey, null); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Collections.emptyMap(), - randomNonNegativeLong(), randomNonNegativeLong()); - - PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policyName, retryableStep, indexName); - Settings.Builder indexSettingsBuilder = Settings.builder() - .put(LifecycleSettings.LIFECYCLE_NAME, policyName); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(errorStepKey.getPhase()); - lifecycleState.setPhaseTime(now); - lifecycleState.setAction(errorStepKey.getAction()); - lifecycleState.setActionTime(now); - lifecycleState.setStep(errorStepKey.getName()); - lifecycleState.setStepTime(now); - lifecycleState.setFailedStep(failedStepKey.getName()); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), - Collections.singletonList(policyMetadata)); - Index index = clusterState.metaData().index(indexName).getIndex(); - IndexLifecycleRunner runner = new IndexLifecycleRunner(policyRegistry, null, threadPool, () -> now); - ClusterState nextClusterState = runner.moveClusterStateToPreviouslyFailedStep(clusterState, indexName, true); - IndexLifecycleRunnerTests.assertClusterStateOnNextStep(clusterState, index, errorStepKey, failedStepKey, - nextClusterState, now); - LifecycleExecutionState executionState = LifecycleExecutionState.fromIndexMetadata(nextClusterState.metaData().index(indexName)); - assertThat(executionState.getFailedStepRetryCount(), is(1)); - } - - public void testAddStepInfoToClusterState() throws IOException { - String indexName = "my_index"; - StepKey currentStep = new StepKey("current_phase", "current_action", "current_step"); - RandomStepInfo stepInfo = new RandomStepInfo(() -> randomAlphaOfLength(10)); - - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStep.getPhase()); - lifecycleState.setAction(currentStep.getAction()); - lifecycleState.setStep(currentStep.getName()); - ClusterState clusterState = buildClusterState(indexName, Settings.builder(), lifecycleState.build(), Collections.emptyList()); - Index index = clusterState.metaData().index(indexName).getIndex(); - ClusterState newClusterState = IndexLifecycleRunner.addStepInfoToClusterState(index, clusterState, stepInfo); - assertClusterStateStepInfo(clusterState, index, currentStep, newClusterState, stepInfo); - ClusterState runAgainClusterState = IndexLifecycleRunner.addStepInfoToClusterState(index, newClusterState, stepInfo); - assertSame(newClusterState, runAgainClusterState); - } - - private ClusterState buildClusterState(String indexName, Settings.Builder indexSettingsBuilder, - LifecycleExecutionState lifecycleState, - List lifecyclePolicyMetadatas) { - Settings indexSettings = indexSettingsBuilder.put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) - .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0).put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build(); - IndexMetaData indexMetadata = IndexMetaData.builder(indexName) - .settings(indexSettings) - .putCustom(ILM_CUSTOM_METADATA_KEY, lifecycleState.asMap()) - .build(); - - Map lifecyclePolicyMetadatasMap = lifecyclePolicyMetadatas.stream() - .collect(Collectors.toMap(LifecyclePolicyMetadata::getName, Function.identity())); - IndexLifecycleMetadata indexLifecycleMetadata = new IndexLifecycleMetadata(lifecyclePolicyMetadatasMap, OperationMode.RUNNING); - - MetaData metadata = MetaData.builder().put(indexMetadata, true).putCustom(IndexLifecycleMetadata.TYPE, indexLifecycleMetadata) - .build(); - return ClusterState.builder(new ClusterName("my_cluster")).metaData(metadata).build(); - } - - private static LifecyclePolicy createPolicy(String policyName, StepKey safeStep, StepKey unsafeStep) { - Map phases = new HashMap<>(); - if (safeStep != null) { - assert MockAction.NAME.equals(safeStep.getAction()) : "The safe action needs to be MockAction.NAME"; - assert unsafeStep == null - || safeStep.getPhase().equals(unsafeStep.getPhase()) == false : "safe and unsafe actions must be in different phases"; - Map actions = new HashMap<>(); - List steps = Collections.singletonList(new MockStep(safeStep, null)); - MockAction safeAction = new MockAction(steps, true); - actions.put(safeAction.getWriteableName(), safeAction); - Phase phase = new Phase(safeStep.getPhase(), TimeValue.timeValueMillis(0), actions); - phases.put(phase.getName(), phase); - } - if (unsafeStep != null) { - assert MockAction.NAME.equals(unsafeStep.getAction()) : "The unsafe action needs to be MockAction.NAME"; - Map actions = new HashMap<>(); - List steps = Collections.singletonList(new MockStep(unsafeStep, null)); - MockAction unsafeAction = new MockAction(steps, false); - actions.put(unsafeAction.getWriteableName(), unsafeAction); - Phase phase = new Phase(unsafeStep.getPhase(), TimeValue.timeValueMillis(0), actions); - phases.put(phase.getName(), phase); - } - return newTestLifecyclePolicy(policyName, phases); - } - - public void testRemovePolicyForIndex() { - String indexName = randomAlphaOfLength(10); - String oldPolicyName = "old_policy"; - StepKey currentStep = new StepKey(randomAlphaOfLength(10), MockAction.NAME, randomAlphaOfLength(10)); - LifecyclePolicy oldPolicy = createPolicy(oldPolicyName, currentStep, null); - Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, oldPolicyName); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStep.getPhase()); - lifecycleState.setAction(currentStep.getAction()); - lifecycleState.setStep(currentStep.getName()); - List policyMetadatas = new ArrayList<>(); - policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), - randomNonNegativeLong(), randomNonNegativeLong())); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); - Index index = clusterState.metaData().index(indexName).getIndex(); - Index[] indices = new Index[] { index }; - List failedIndexes = new ArrayList<>(); - - ClusterState newClusterState = IndexLifecycleRunner.removePolicyForIndexes(indices, clusterState, failedIndexes); - - assertTrue(failedIndexes.isEmpty()); - assertIndexNotManagedByILM(newClusterState, index); - } - - public void testRemovePolicyForIndexNoCurrentPolicy() { - String indexName = randomAlphaOfLength(10); - Settings.Builder indexSettingsBuilder = Settings.builder(); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, LifecycleExecutionState.builder().build(), - Collections.emptyList()); - Index index = clusterState.metaData().index(indexName).getIndex(); - Index[] indices = new Index[] { index }; - List failedIndexes = new ArrayList<>(); - - ClusterState newClusterState = IndexLifecycleRunner.removePolicyForIndexes(indices, clusterState, failedIndexes); - - assertTrue(failedIndexes.isEmpty()); - assertIndexNotManagedByILM(newClusterState, index); - } - - public void testRemovePolicyForIndexIndexDoesntExist() { - String indexName = randomAlphaOfLength(10); - String oldPolicyName = "old_policy"; - LifecyclePolicy oldPolicy = newTestLifecyclePolicy(oldPolicyName, Collections.emptyMap()); - StepKey currentStep = AbstractStepTestCase.randomStepKey(); - Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, oldPolicyName); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStep.getPhase()); - lifecycleState.setAction(currentStep.getAction()); - lifecycleState.setStep(currentStep.getName()); - List policyMetadatas = new ArrayList<>(); - policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), - randomNonNegativeLong(), randomNonNegativeLong())); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); - Index index = new Index("doesnt_exist", "im_not_here"); - Index[] indices = new Index[] { index }; - List failedIndexes = new ArrayList<>(); - - ClusterState newClusterState = IndexLifecycleRunner.removePolicyForIndexes(indices, clusterState, failedIndexes); - - assertEquals(1, failedIndexes.size()); - assertEquals("doesnt_exist", failedIndexes.get(0)); - assertSame(clusterState, newClusterState); - } - - public void testRemovePolicyForIndexIndexInUnsafe() { - String indexName = randomAlphaOfLength(10); - String oldPolicyName = "old_policy"; - StepKey currentStep = new StepKey(randomAlphaOfLength(10), MockAction.NAME, randomAlphaOfLength(10)); - LifecyclePolicy oldPolicy = createPolicy(oldPolicyName, null, currentStep); - Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, oldPolicyName); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStep.getPhase()); - lifecycleState.setAction(currentStep.getAction()); - lifecycleState.setStep(currentStep.getName()); - List policyMetadatas = new ArrayList<>(); - policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), - randomNonNegativeLong(), randomNonNegativeLong())); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); - Index index = clusterState.metaData().index(indexName).getIndex(); - Index[] indices = new Index[] { index }; - List failedIndexes = new ArrayList<>(); - - ClusterState newClusterState = IndexLifecycleRunner.removePolicyForIndexes(indices, clusterState, failedIndexes); - - assertTrue(failedIndexes.isEmpty()); - assertIndexNotManagedByILM(newClusterState, index); - } - - public void testRemovePolicyWithIndexingComplete() { - String indexName = randomAlphaOfLength(10); - String oldPolicyName = "old_policy"; - StepKey currentStep = new StepKey(randomAlphaOfLength(10), MockAction.NAME, randomAlphaOfLength(10)); - LifecyclePolicy oldPolicy = createPolicy(oldPolicyName, null, currentStep); - Settings.Builder indexSettingsBuilder = Settings.builder() - .put(LifecycleSettings.LIFECYCLE_NAME, oldPolicyName) - .put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, true); - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase(currentStep.getPhase()); - lifecycleState.setAction(currentStep.getAction()); - lifecycleState.setStep(currentStep.getName()); - List policyMetadatas = new ArrayList<>(); - policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), - randomNonNegativeLong(), randomNonNegativeLong())); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); - Index index = clusterState.metaData().index(indexName).getIndex(); - Index[] indices = new Index[] { index }; - List failedIndexes = new ArrayList<>(); - - ClusterState newClusterState = IndexLifecycleRunner.removePolicyForIndexes(indices, clusterState, failedIndexes); - - assertTrue(failedIndexes.isEmpty()); - assertIndexNotManagedByILM(newClusterState, index); - } - public void testIsReadyToTransition() { String policyName = "async_action_policy"; StepKey stepKey = new StepKey("phase", MockAction.NAME, MockAction.NAME); @@ -1405,120 +751,29 @@ public void testIsReadyToTransition() { runner.isReadyToTransitionToThisPhase(policyName, indexMetaData, "phase")); } - public void testValidateTransitionThrowsExceptionForMissingIndexPolicy() { - IndexMetaData indexMetaData = IndexMetaData.builder("index").settings(settings(Version.CURRENT)) - .numberOfShards(randomIntBetween(1, 5)) - .numberOfReplicas(randomIntBetween(0, 5)) - .build(); - - StepKey currentStepKey = new StepKey("hot", "action", "firstStep"); - StepKey nextStepKey = new StepKey("hot", "action", "secondStep"); - Step currentStep = new MockStep(currentStepKey, nextStepKey); - MockPolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry("policy", currentStep); - - expectThrows(IllegalArgumentException.class, - () -> IndexLifecycleRunner.validateTransition(indexMetaData, currentStepKey, nextStepKey, policyRegistry)); - } - - public void testValidateTransitionThrowsExceptionIfTheCurrentStepIsIncorrect() { - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase("hot"); - lifecycleState.setAction("action"); - lifecycleState.setStep("another_step"); - String policy = "policy"; - IndexMetaData indexMetaData = buildIndexMetadata(policy, lifecycleState); - - StepKey currentStepKey = new StepKey("hot", "action", "firstStep"); - StepKey nextStepKey = new StepKey("hot", "action", "secondStep"); - Step currentStep = new MockStep(currentStepKey, nextStepKey); - MockPolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policy, currentStep); - - expectThrows(IllegalArgumentException.class, - () -> IndexLifecycleRunner.validateTransition(indexMetaData, currentStepKey, nextStepKey, policyRegistry)); - } - - public void testValidateTransitionThrowsExceptionIfNextStepDoesNotExist() { - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase("hot"); - lifecycleState.setAction("action"); - lifecycleState.setStep("firstStep"); - String policy = "policy"; - IndexMetaData indexMetaData = buildIndexMetadata(policy, lifecycleState); - - StepKey currentStepKey = new StepKey("hot", "action", "firstStep"); - StepKey nextStepKey = new StepKey("hot", "action", "secondStep"); - Step currentStep = new MockStep(currentStepKey, nextStepKey); - MockPolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policy, currentStep); - - expectThrows(IllegalArgumentException.class, - () -> IndexLifecycleRunner.validateTransition(indexMetaData, currentStepKey, nextStepKey, policyRegistry)); - } - - public void testValidateValidTransition() { - LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); - lifecycleState.setPhase("hot"); - lifecycleState.setAction("action"); - lifecycleState.setStep("firstStep"); - String policy = "policy"; - IndexMetaData indexMetaData = buildIndexMetadata(policy, lifecycleState); - - StepKey currentStepKey = new StepKey("hot", "action", "firstStep"); - StepKey nextStepKey = new StepKey("hot", "action", "secondStep"); - Step finalStep = new MockStep(nextStepKey, new StepKey("hot", "action", "completed")); - MockPolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policy, finalStep); - - try { - IndexLifecycleRunner.validateTransition(indexMetaData, currentStepKey, nextStepKey, policyRegistry); - } catch (Exception e) { - logger.error(e); - fail("validateTransition should not throw exception on valid transitions"); - } - } - - public static void assertIndexNotManagedByILM(ClusterState clusterState, Index index) { - MetaData metadata = clusterState.metaData(); - assertNotNull(metadata); - IndexMetaData indexMetadata = metadata.getIndexSafe(index); - assertNotNull(indexMetadata); - Settings indexSettings = indexMetadata.getSettings(); - assertNotNull(indexSettings); - assertFalse(LifecycleSettings.LIFECYCLE_NAME_SETTING.exists(indexSettings)); - assertFalse(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS_SETTING.exists(indexSettings)); - assertFalse(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE_SETTING.exists(indexSettings)); - } - - public static void assertClusterStateOnPolicy(ClusterState oldClusterState, Index index, String expectedPolicy, StepKey previousStep, - StepKey expectedStep, ClusterState newClusterState, long now) { - assertNotSame(oldClusterState, newClusterState); - MetaData newMetadata = newClusterState.metaData(); - assertNotSame(oldClusterState.metaData(), newMetadata); - IndexMetaData newIndexMetadata = newMetadata.getIndexSafe(index); - assertNotSame(oldClusterState.metaData().index(index), newIndexMetadata); - LifecycleExecutionState newLifecycleState = LifecycleExecutionState - .fromIndexMetadata(newClusterState.metaData().index(index)); - LifecycleExecutionState oldLifecycleState = LifecycleExecutionState - .fromIndexMetadata(oldClusterState.metaData().index(index)); - assertNotSame(oldLifecycleState, newLifecycleState); - assertEquals(expectedStep.getPhase(), newLifecycleState.getPhase()); - assertEquals(expectedStep.getAction(), newLifecycleState.getAction()); - assertEquals(expectedStep.getName(), newLifecycleState.getStep()); - if (Objects.equals(previousStep.getPhase(), expectedStep.getPhase())) { - assertEquals(oldLifecycleState.getPhase(), newLifecycleState.getPhase()); - } else { - assertEquals(now, newLifecycleState.getPhaseTime().longValue()); - } - if (Objects.equals(previousStep.getAction(), expectedStep.getAction())) { - assertEquals(oldLifecycleState.getActionTime(), newLifecycleState.getActionTime()); - } else { - assertEquals(now, newLifecycleState.getActionTime().longValue()); + private static LifecyclePolicy createPolicy(String policyName, StepKey safeStep, StepKey unsafeStep) { + Map phases = new HashMap<>(); + if (safeStep != null) { + assert MockAction.NAME.equals(safeStep.getAction()) : "The safe action needs to be MockAction.NAME"; + assert unsafeStep == null + || safeStep.getPhase().equals(unsafeStep.getPhase()) == false : "safe and unsafe actions must be in different phases"; + Map actions = new HashMap<>(); + List steps = Collections.singletonList(new MockStep(safeStep, null)); + MockAction safeAction = new MockAction(steps, true); + actions.put(safeAction.getWriteableName(), safeAction); + Phase phase = new Phase(safeStep.getPhase(), TimeValue.timeValueMillis(0), actions); + phases.put(phase.getName(), phase); } - if (Objects.equals(previousStep.getName(), expectedStep.getName())) { - assertEquals(oldLifecycleState.getStepTime(), newLifecycleState.getStepTime()); - } else { - assertEquals(now, newLifecycleState.getStepTime().longValue()); + if (unsafeStep != null) { + assert MockAction.NAME.equals(unsafeStep.getAction()) : "The unsafe action needs to be MockAction.NAME"; + Map actions = new HashMap<>(); + List steps = Collections.singletonList(new MockStep(unsafeStep, null)); + MockAction unsafeAction = new MockAction(steps, false); + actions.put(unsafeAction.getWriteableName(), unsafeAction); + Phase phase = new Phase(unsafeStep.getPhase(), TimeValue.timeValueMillis(0), actions); + phases.put(phase.getName(), phase); } - assertEquals(null, newLifecycleState.getFailedStep()); - assertEquals(null, newLifecycleState.getStepInfo()); + return newTestLifecyclePolicy(policyName, phases); } public static void assertClusterStateOnNextStep(ClusterState oldClusterState, Index index, StepKey currentStep, StepKey nextStep, @@ -1551,61 +806,6 @@ public static void assertClusterStateOnNextStep(ClusterState oldClusterState, In assertEquals(null, newLifecycleState.getStepInfo()); } - private IndexMetaData buildIndexMetadata(String policy, LifecycleExecutionState.Builder lifecycleState) { - return IndexMetaData.builder("index") - .settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policy)) - .numberOfShards(randomIntBetween(1, 5)) - .numberOfReplicas(randomIntBetween(0, 5)) - .putCustom(ILM_CUSTOM_METADATA_KEY, lifecycleState.build().asMap()) - .build(); - } - - private void assertClusterStateOnErrorStep(ClusterState oldClusterState, Index index, StepKey currentStep, - ClusterState newClusterState, long now, String expectedCauseValue) throws IOException { - assertNotSame(oldClusterState, newClusterState); - MetaData newMetadata = newClusterState.metaData(); - assertNotSame(oldClusterState.metaData(), newMetadata); - IndexMetaData newIndexMetadata = newMetadata.getIndexSafe(index); - assertNotSame(oldClusterState.metaData().index(index), newIndexMetadata); - LifecycleExecutionState newLifecycleState = LifecycleExecutionState - .fromIndexMetadata(newClusterState.metaData().index(index)); - LifecycleExecutionState oldLifecycleState = LifecycleExecutionState - .fromIndexMetadata(oldClusterState.metaData().index(index)); - assertNotSame(oldLifecycleState, newLifecycleState); - assertEquals(currentStep.getPhase(), newLifecycleState.getPhase()); - assertEquals(currentStep.getAction(), newLifecycleState.getAction()); - assertEquals(ErrorStep.NAME, newLifecycleState.getStep()); - assertEquals(currentStep.getName(), newLifecycleState.getFailedStep()); - assertThat(newLifecycleState.getStepInfo(), containsString(expectedCauseValue)); - assertEquals(oldLifecycleState.getPhaseTime(), newLifecycleState.getPhaseTime()); - assertEquals(oldLifecycleState.getActionTime(), newLifecycleState.getActionTime()); - assertEquals(now, newLifecycleState.getStepTime().longValue()); - } - - private void assertClusterStateStepInfo(ClusterState oldClusterState, Index index, StepKey currentStep, ClusterState newClusterState, - ToXContentObject stepInfo) throws IOException { - XContentBuilder stepInfoXContentBuilder = JsonXContent.contentBuilder(); - stepInfo.toXContent(stepInfoXContentBuilder, ToXContent.EMPTY_PARAMS); - String expectedstepInfoValue = BytesReference.bytes(stepInfoXContentBuilder).utf8ToString(); - assertNotSame(oldClusterState, newClusterState); - MetaData newMetadata = newClusterState.metaData(); - assertNotSame(oldClusterState.metaData(), newMetadata); - IndexMetaData newIndexMetadata = newMetadata.getIndexSafe(index); - assertNotSame(oldClusterState.metaData().index(index), newIndexMetadata); - LifecycleExecutionState newLifecycleState = LifecycleExecutionState - .fromIndexMetadata(newClusterState.metaData().index(index)); - LifecycleExecutionState oldLifecycleState = LifecycleExecutionState - .fromIndexMetadata(oldClusterState.metaData().index(index)); - assertNotSame(oldLifecycleState, newLifecycleState); - assertEquals(currentStep.getPhase(), newLifecycleState.getPhase()); - assertEquals(currentStep.getAction(), newLifecycleState.getAction()); - assertEquals(currentStep.getName(), newLifecycleState.getStep()); - assertEquals(expectedstepInfoValue, newLifecycleState.getStepInfo()); - assertEquals(oldLifecycleState.getPhaseTime(), newLifecycleState.getPhaseTime()); - assertEquals(oldLifecycleState.getActionTime(), newLifecycleState.getActionTime()); - assertEquals(newLifecycleState.getStepTime(), newLifecycleState.getStepTime()); - } - static class MockAsyncActionStep extends AsyncActionStep { private Exception exception; @@ -1830,7 +1030,7 @@ public boolean matches(Object argument) { } - private static final class RetryableMockStep extends MockStep { + static final class RetryableMockStep extends MockStep { RetryableMockStep(StepKey stepKey, StepKey nextStepKey) { super(stepKey, nextStepKey); @@ -1841,4 +1041,49 @@ public boolean isRetryable() { return true; } } + + /** A real policy steps registry where getStep can be overridden so that JSON doesn't have to be parsed */ + public static class MockPolicyStepsRegistry extends PolicyStepsRegistry { + private BiFunction fn = null; + private static Logger logger = LogManager.getLogger(MockPolicyStepsRegistry.class); + + MockPolicyStepsRegistry(SortedMap lifecyclePolicyMap, Map firstStepMap, + Map> stepMap, NamedXContentRegistry xContentRegistry, Client client) { + super(lifecyclePolicyMap, firstStepMap, stepMap, xContentRegistry, client); + } + + public void setResolver(BiFunction fn) { + this.fn = fn; + } + + @Override + public Step getStep(IndexMetaData indexMetaData, StepKey stepKey) { + if (fn == null) { + logger.info("--> retrieving step {}", stepKey); + return super.getStep(indexMetaData, stepKey); + } else { + logger.info("--> returning mock step"); + return fn.apply(indexMetaData, stepKey); + } + } + } + + public static MockPolicyStepsRegistry createOneStepPolicyStepRegistry(String policyName, Step step) { + return createMultiStepPolicyStepRegistry(policyName, Collections.singletonList(step)); + } + + public static MockPolicyStepsRegistry createMultiStepPolicyStepRegistry(String policyName, List steps) { + LifecyclePolicy policy = new LifecyclePolicy(policyName, new HashMap<>()); + SortedMap lifecyclePolicyMap = new TreeMap<>(); + lifecyclePolicyMap.put(policyName, new LifecyclePolicyMetadata(policy, new HashMap<>(), 1, 1)); + Map firstStepMap = new HashMap<>(); + firstStepMap.put(policyName, steps.get(0)); + Map> stepMap = new HashMap<>(); + Map policySteps = new HashMap<>(); + steps.forEach(step -> policySteps.put(step.getKey(), step)); + stepMap.put(policyName, policySteps); + Client client = mock(Client.class); + when(client.settings()).thenReturn(Settings.EMPTY); + return new MockPolicyStepsRegistry(lifecyclePolicyMap, firstStepMap, stepMap, REGISTRY, client); + } } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java new file mode 100644 index 0000000000000..a882bc913832d --- /dev/null +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java @@ -0,0 +1,784 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.ilm; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.index.Index; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.ilm.AbstractStepTestCase; +import org.elasticsearch.xpack.core.ilm.ErrorStep; +import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; +import org.elasticsearch.xpack.core.ilm.LifecycleAction; +import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicyTests; +import org.elasticsearch.xpack.core.ilm.LifecycleSettings; +import org.elasticsearch.xpack.core.ilm.MockAction; +import org.elasticsearch.xpack.core.ilm.MockStep; +import org.elasticsearch.xpack.core.ilm.OperationMode; +import org.elasticsearch.xpack.core.ilm.Phase; +import org.elasticsearch.xpack.core.ilm.RolloverAction; +import org.elasticsearch.xpack.core.ilm.Step; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; +import static org.elasticsearch.xpack.core.ilm.LifecyclePolicyTestsUtils.newTestLifecyclePolicy; +import static org.elasticsearch.xpack.ilm.IndexLifecycleRunnerTests.createOneStepPolicyStepRegistry; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +public class IndexLifecycleTransitionTests extends ESTestCase { + + public void testMoveClusterStateToNextStep() { + String indexName = "my_index"; + LifecyclePolicy policy = randomValueOtherThanMany(p -> p.getPhases().size() == 0, + () -> LifecyclePolicyTests.randomTestLifecyclePolicy("policy")); + Phase nextPhase = policy.getPhases().values().stream() + .findFirst().orElseThrow(() -> new AssertionError("expected next phase to be present")); + List policyMetadatas = Collections.singletonList( + new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong())); + Step.StepKey currentStep = new Step.StepKey("current_phase", "current_action", "current_step"); + Step.StepKey nextStep = new Step.StepKey(nextPhase.getName(), "next_action", "next_step"); + long now = randomNonNegativeLong(); + + // test going from null lifecycle settings to next step + ClusterState clusterState = buildClusterState(indexName, + Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, policy.getName()), LifecycleExecutionState.builder().build(), policyMetadatas); + Index index = clusterState.metaData().index(indexName).getIndex(); + PolicyStepsRegistry stepsRegistry = createOneStepPolicyStepRegistry(policy.getName(), + new MockStep(nextStep, nextStep)); + ClusterState newClusterState = IndexLifecycleTransition.moveClusterStateToStep(index, clusterState, nextStep, + () -> now, stepsRegistry, false); + assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); + + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStep.getPhase()); + lifecycleState.setAction(currentStep.getAction()); + lifecycleState.setStep(currentStep.getName()); + // test going from set currentStep settings to nextStep + Settings.Builder indexSettingsBuilder = Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, policy.getName()); + if (randomBoolean()) { + lifecycleState.setStepInfo(randomAlphaOfLength(20)); + } + + clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); + index = clusterState.metaData().index(indexName).getIndex(); + newClusterState = IndexLifecycleTransition.moveClusterStateToStep(index, clusterState, + nextStep, () -> now, stepsRegistry, false); + assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); + } + + public void testMoveClusterStateToNextStepSamePhase() { + String indexName = "my_index"; + LifecyclePolicy policy = randomValueOtherThanMany(p -> p.getPhases().size() == 0, + () -> LifecyclePolicyTests.randomTestLifecyclePolicy("policy")); + List policyMetadatas = Collections.singletonList( + new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong())); + Step.StepKey currentStep = new Step.StepKey("current_phase", "current_action", "current_step"); + Step.StepKey nextStep = new Step.StepKey("current_phase", "next_action", "next_step"); + long now = randomNonNegativeLong(); + + ClusterState clusterState = buildClusterState(indexName, + Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, policy.getName()), + LifecycleExecutionState.builder() + .setPhase(currentStep.getPhase()) + .setAction(currentStep.getAction()) + .setStep(currentStep.getName()) + .build(), policyMetadatas); + Index index = clusterState.metaData().index(indexName).getIndex(); + PolicyStepsRegistry stepsRegistry = createOneStepPolicyStepRegistry(policy.getName(), + new MockStep(nextStep, nextStep)); + ClusterState newClusterState = IndexLifecycleTransition.moveClusterStateToStep(index, clusterState, nextStep, + () -> now, stepsRegistry, false); + assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); + + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStep.getPhase()); + lifecycleState.setAction(currentStep.getAction()); + lifecycleState.setStep(currentStep.getName()); + if (randomBoolean()) { + lifecycleState.setStepInfo(randomAlphaOfLength(20)); + } + + Settings.Builder indexSettingsBuilder = Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, policy.getName()); + + clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); + index = clusterState.metaData().index(indexName).getIndex(); + newClusterState = IndexLifecycleTransition.moveClusterStateToStep(index, clusterState, nextStep, + () -> now, stepsRegistry, false); + assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); + } + + public void testMoveClusterStateToNextStepSameAction() { + String indexName = "my_index"; + LifecyclePolicy policy = randomValueOtherThanMany(p -> p.getPhases().size() == 0, + () -> LifecyclePolicyTests.randomTestLifecyclePolicy("policy")); + List policyMetadatas = Collections.singletonList( + new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong())); + Step.StepKey currentStep = new Step.StepKey("current_phase", "current_action", "current_step"); + Step.StepKey nextStep = new Step.StepKey("current_phase", "current_action", "next_step"); + long now = randomNonNegativeLong(); + + ClusterState clusterState = buildClusterState(indexName, + Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, policy.getName()), + LifecycleExecutionState.builder() + .setPhase(currentStep.getPhase()) + .setAction(currentStep.getAction()) + .setStep(currentStep.getName()) + .build(), policyMetadatas); + Index index = clusterState.metaData().index(indexName).getIndex(); + PolicyStepsRegistry stepsRegistry = createOneStepPolicyStepRegistry(policy.getName(), + new MockStep(nextStep, nextStep)); + ClusterState newClusterState = IndexLifecycleTransition.moveClusterStateToStep(index, clusterState, nextStep, + () -> now, stepsRegistry, false); + assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); + + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStep.getPhase()); + lifecycleState.setAction(currentStep.getAction()); + lifecycleState.setStep(currentStep.getName()); + if (randomBoolean()) { + lifecycleState.setStepInfo(randomAlphaOfLength(20)); + } + + Settings.Builder indexSettingsBuilder = Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, policy.getName()); + + clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); + index = clusterState.metaData().index(indexName).getIndex(); + newClusterState = IndexLifecycleTransition.moveClusterStateToStep(index, clusterState, nextStep, + () -> now, stepsRegistry, false); + assertClusterStateOnNextStep(clusterState, index, currentStep, nextStep, newClusterState, now); + } + + public void testSuccessfulValidatedMoveClusterStateToNextStep() { + String indexName = "my_index"; + String policyName = "my_policy"; + LifecyclePolicy policy = randomValueOtherThanMany(p -> p.getPhases().size() == 0, + () -> LifecyclePolicyTests.randomTestLifecyclePolicy(policyName)); + Phase nextPhase = policy.getPhases().values().stream() + .findFirst().orElseThrow(() -> new AssertionError("expected next phase to be present")); + List policyMetadatas = Collections.singletonList( + new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong())); + Step.StepKey currentStepKey = new Step.StepKey("current_phase", "current_action", "current_step"); + Step.StepKey nextStepKey = new Step.StepKey(nextPhase.getName(), "next_action", "next_step"); + long now = randomNonNegativeLong(); + Step step = new MockStep(nextStepKey, nextStepKey); + PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); + + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStepKey.getPhase()); + lifecycleState.setAction(currentStepKey.getAction()); + lifecycleState.setStep(currentStepKey.getName()); + + Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, policyName); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); + Index index = clusterState.metaData().index(indexName).getIndex(); + ClusterState newClusterState = IndexLifecycleTransition.moveClusterStateToStep(index, clusterState, + nextStepKey, () -> now, stepRegistry, true); + assertClusterStateOnNextStep(clusterState, index, currentStepKey, nextStepKey, newClusterState, now); + } + + public void testValidatedMoveClusterStateToNextStepWithoutPolicy() { + String indexName = "my_index"; + String policyName = "policy"; + Step.StepKey currentStepKey = new Step.StepKey("current_phase", "current_action", "current_step"); + Step.StepKey nextStepKey = new Step.StepKey("next_phase", "next_action", "next_step"); + long now = randomNonNegativeLong(); + Step step = new MockStep(nextStepKey, nextStepKey); + PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); + + Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, randomBoolean() ? "" : null); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStepKey.getPhase()); + lifecycleState.setAction(currentStepKey.getAction()); + lifecycleState.setStep(currentStepKey.getName()); + + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); + Index index = clusterState.metaData().index(indexName).getIndex(); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> IndexLifecycleTransition.moveClusterStateToStep(index, clusterState, nextStepKey, () -> now, stepRegistry, true)); + assertThat(exception.getMessage(), equalTo("index [my_index] is not associated with an Index Lifecycle Policy")); + } + + public void testValidatedMoveClusterStateToNextStepInvalidNextStep() { + String indexName = "my_index"; + String policyName = "my_policy"; + Step.StepKey currentStepKey = new Step.StepKey("current_phase", "current_action", "current_step"); + Step.StepKey nextStepKey = new Step.StepKey("next_phase", "next_action", "next_step"); + long now = randomNonNegativeLong(); + Step step = new MockStep(currentStepKey, nextStepKey); + PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); + + Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, policyName); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStepKey.getPhase()); + lifecycleState.setAction(currentStepKey.getAction()); + lifecycleState.setStep(currentStepKey.getName()); + + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); + Index index = clusterState.metaData().index(indexName).getIndex(); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> IndexLifecycleTransition.moveClusterStateToStep(index, clusterState, nextStepKey, () -> now, stepRegistry, true)); + assertThat(exception.getMessage(), + equalTo("step [{\"phase\":\"next_phase\",\"action\":\"next_action\",\"name\":\"next_step\"}] " + + "for index [my_index] with policy [my_policy] does not exist")); + } + + public void testMoveClusterStateToErrorStep() throws IOException { + String indexName = "my_index"; + Step.StepKey currentStep = new Step.StepKey("current_phase", "current_action", "current_step"); + Step.StepKey nextStepKey = new Step.StepKey("next_phase", "next_action", "next_step"); + long now = randomNonNegativeLong(); + Exception cause = new ElasticsearchException("THIS IS AN EXPECTED CAUSE"); + + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStep.getPhase()); + lifecycleState.setAction(currentStep.getAction()); + lifecycleState.setStep(currentStep.getName()); + ClusterState clusterState = buildClusterState(indexName, Settings.builder(), lifecycleState.build(), Collections.emptyList()); + Index index = clusterState.metaData().index(indexName).getIndex(); + + ClusterState newClusterState = IndexLifecycleTransition.moveClusterStateToErrorStep(index, clusterState, cause, + () -> now, (idxMeta, stepKey) -> new MockStep(stepKey, nextStepKey)); + assertClusterStateOnErrorStep(clusterState, index, currentStep, newClusterState, now, + "{\"type\":\"exception\",\"reason\":\"THIS IS AN EXPECTED CAUSE\""); + + cause = new IllegalArgumentException("non elasticsearch-exception"); + newClusterState = IndexLifecycleTransition.moveClusterStateToErrorStep(index, clusterState, cause, () -> now, + (idxMeta, stepKey) -> new MockStep(stepKey, nextStepKey)); + assertClusterStateOnErrorStep(clusterState, index, currentStep, newClusterState, now, + "{\"type\":\"illegal_argument_exception\",\"reason\":\"non elasticsearch-exception\",\"stack_trace\":\""); + } + + public void testAddStepInfoToClusterState() throws IOException { + String indexName = "my_index"; + Step.StepKey currentStep = new Step.StepKey("current_phase", "current_action", "current_step"); + RandomStepInfo stepInfo = new RandomStepInfo(() -> randomAlphaOfLength(10)); + + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStep.getPhase()); + lifecycleState.setAction(currentStep.getAction()); + lifecycleState.setStep(currentStep.getName()); + ClusterState clusterState = buildClusterState(indexName, Settings.builder(), lifecycleState.build(), Collections.emptyList()); + Index index = clusterState.metaData().index(indexName).getIndex(); + ClusterState newClusterState = IndexLifecycleTransition.addStepInfoToClusterState(index, clusterState, stepInfo); + assertClusterStateStepInfo(clusterState, index, currentStep, newClusterState, stepInfo); + ClusterState runAgainClusterState = IndexLifecycleTransition.addStepInfoToClusterState(index, newClusterState, stepInfo); + assertSame(newClusterState, runAgainClusterState); + } + + + public void testRemovePolicyForIndex() { + String indexName = randomAlphaOfLength(10); + String oldPolicyName = "old_policy"; + Step.StepKey currentStep = new Step.StepKey(randomAlphaOfLength(10), MockAction.NAME, randomAlphaOfLength(10)); + LifecyclePolicy oldPolicy = createPolicy(oldPolicyName, currentStep, null); + Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, oldPolicyName); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStep.getPhase()); + lifecycleState.setAction(currentStep.getAction()); + lifecycleState.setStep(currentStep.getName()); + List policyMetadatas = new ArrayList<>(); + policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), + randomNonNegativeLong(), randomNonNegativeLong())); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); + Index index = clusterState.metaData().index(indexName).getIndex(); + Index[] indices = new Index[] { index }; + List failedIndexes = new ArrayList<>(); + + ClusterState newClusterState = IndexLifecycleTransition.removePolicyForIndexes(indices, clusterState, failedIndexes); + + assertTrue(failedIndexes.isEmpty()); + assertIndexNotManagedByILM(newClusterState, index); + } + + public void testRemovePolicyForIndexNoCurrentPolicy() { + String indexName = randomAlphaOfLength(10); + Settings.Builder indexSettingsBuilder = Settings.builder(); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, LifecycleExecutionState.builder().build(), + Collections.emptyList()); + Index index = clusterState.metaData().index(indexName).getIndex(); + Index[] indices = new Index[] { index }; + List failedIndexes = new ArrayList<>(); + + ClusterState newClusterState = IndexLifecycleTransition.removePolicyForIndexes(indices, clusterState, failedIndexes); + + assertTrue(failedIndexes.isEmpty()); + assertIndexNotManagedByILM(newClusterState, index); + } + + public void testRemovePolicyForIndexIndexDoesntExist() { + String indexName = randomAlphaOfLength(10); + String oldPolicyName = "old_policy"; + LifecyclePolicy oldPolicy = newTestLifecyclePolicy(oldPolicyName, Collections.emptyMap()); + Step.StepKey currentStep = AbstractStepTestCase.randomStepKey(); + Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, oldPolicyName); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStep.getPhase()); + lifecycleState.setAction(currentStep.getAction()); + lifecycleState.setStep(currentStep.getName()); + List policyMetadatas = new ArrayList<>(); + policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), + randomNonNegativeLong(), randomNonNegativeLong())); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); + Index index = new Index("doesnt_exist", "im_not_here"); + Index[] indices = new Index[] { index }; + List failedIndexes = new ArrayList<>(); + + ClusterState newClusterState = IndexLifecycleTransition.removePolicyForIndexes(indices, clusterState, failedIndexes); + + assertEquals(1, failedIndexes.size()); + assertEquals("doesnt_exist", failedIndexes.get(0)); + assertSame(clusterState, newClusterState); + } + + public void testRemovePolicyForIndexIndexInUnsafe() { + String indexName = randomAlphaOfLength(10); + String oldPolicyName = "old_policy"; + Step.StepKey currentStep = new Step.StepKey(randomAlphaOfLength(10), MockAction.NAME, randomAlphaOfLength(10)); + LifecyclePolicy oldPolicy = createPolicy(oldPolicyName, null, currentStep); + Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, oldPolicyName); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStep.getPhase()); + lifecycleState.setAction(currentStep.getAction()); + lifecycleState.setStep(currentStep.getName()); + List policyMetadatas = new ArrayList<>(); + policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), + randomNonNegativeLong(), randomNonNegativeLong())); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); + Index index = clusterState.metaData().index(indexName).getIndex(); + Index[] indices = new Index[] { index }; + List failedIndexes = new ArrayList<>(); + + ClusterState newClusterState = IndexLifecycleTransition.removePolicyForIndexes(indices, clusterState, failedIndexes); + + assertTrue(failedIndexes.isEmpty()); + assertIndexNotManagedByILM(newClusterState, index); + } + + public void testRemovePolicyWithIndexingComplete() { + String indexName = randomAlphaOfLength(10); + String oldPolicyName = "old_policy"; + Step.StepKey currentStep = new Step.StepKey(randomAlphaOfLength(10), MockAction.NAME, randomAlphaOfLength(10)); + LifecyclePolicy oldPolicy = createPolicy(oldPolicyName, null, currentStep); + Settings.Builder indexSettingsBuilder = Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, oldPolicyName) + .put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, true); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(currentStep.getPhase()); + lifecycleState.setAction(currentStep.getAction()); + lifecycleState.setStep(currentStep.getName()); + List policyMetadatas = new ArrayList<>(); + policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), + randomNonNegativeLong(), randomNonNegativeLong())); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); + Index index = clusterState.metaData().index(indexName).getIndex(); + Index[] indices = new Index[] { index }; + List failedIndexes = new ArrayList<>(); + + ClusterState newClusterState = IndexLifecycleTransition.removePolicyForIndexes(indices, clusterState, failedIndexes); + + assertTrue(failedIndexes.isEmpty()); + assertIndexNotManagedByILM(newClusterState, index); + } + + public void testValidateTransitionThrowsExceptionForMissingIndexPolicy() { + IndexMetaData indexMetaData = IndexMetaData.builder("index").settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1, 5)) + .numberOfReplicas(randomIntBetween(0, 5)) + .build(); + + Step.StepKey currentStepKey = new Step.StepKey("hot", "action", "firstStep"); + Step.StepKey nextStepKey = new Step.StepKey("hot", "action", "secondStep"); + Step currentStep = new MockStep(currentStepKey, nextStepKey); + PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry("policy", currentStep); + + expectThrows(IllegalArgumentException.class, + () -> IndexLifecycleTransition.validateTransition(indexMetaData, currentStepKey, nextStepKey, policyRegistry)); + } + + public void testValidateTransitionThrowsExceptionIfTheCurrentStepIsIncorrect() { + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase("hot"); + lifecycleState.setAction("action"); + lifecycleState.setStep("another_step"); + String policy = "policy"; + IndexMetaData indexMetaData = buildIndexMetadata(policy, lifecycleState); + + Step.StepKey currentStepKey = new Step.StepKey("hot", "action", "firstStep"); + Step.StepKey nextStepKey = new Step.StepKey("hot", "action", "secondStep"); + Step currentStep = new MockStep(currentStepKey, nextStepKey); + PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policy, currentStep); + + expectThrows(IllegalArgumentException.class, + () -> IndexLifecycleTransition.validateTransition(indexMetaData, currentStepKey, nextStepKey, policyRegistry)); + } + + public void testValidateTransitionThrowsExceptionIfNextStepDoesNotExist() { + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase("hot"); + lifecycleState.setAction("action"); + lifecycleState.setStep("firstStep"); + String policy = "policy"; + IndexMetaData indexMetaData = buildIndexMetadata(policy, lifecycleState); + + Step.StepKey currentStepKey = new Step.StepKey("hot", "action", "firstStep"); + Step.StepKey nextStepKey = new Step.StepKey("hot", "action", "secondStep"); + Step currentStep = new MockStep(currentStepKey, nextStepKey); + PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policy, currentStep); + + expectThrows(IllegalArgumentException.class, + () -> IndexLifecycleTransition.validateTransition(indexMetaData, currentStepKey, nextStepKey, policyRegistry)); + } + + public void testValidateValidTransition() { + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase("hot"); + lifecycleState.setAction("action"); + lifecycleState.setStep("firstStep"); + String policy = "policy"; + IndexMetaData indexMetaData = buildIndexMetadata(policy, lifecycleState); + + Step.StepKey currentStepKey = new Step.StepKey("hot", "action", "firstStep"); + Step.StepKey nextStepKey = new Step.StepKey("hot", "action", "secondStep"); + Step finalStep = new MockStep(nextStepKey, new Step.StepKey("hot", "action", "completed")); + PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policy, finalStep); + + try { + IndexLifecycleTransition.validateTransition(indexMetaData, currentStepKey, nextStepKey, policyRegistry); + } catch (Exception e) { + logger.error(e); + fail("validateTransition should not throw exception on valid transitions"); + } + } + + public void testMoveClusterStateToFailedStep() { + String indexName = "my_index"; + String policyName = "my_policy"; + long now = randomNonNegativeLong(); + Step.StepKey failedStepKey = new Step.StepKey("current_phase", MockAction.NAME, "current_step"); + Step.StepKey errorStepKey = new Step.StepKey(failedStepKey.getPhase(), failedStepKey.getAction(), ErrorStep.NAME); + Step step = new MockStep(failedStepKey, null); + LifecyclePolicy policy = createPolicy(policyName, failedStepKey, null); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Collections.emptyMap(), + randomNonNegativeLong(), randomNonNegativeLong()); + + PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policyName, step); + Settings.Builder indexSettingsBuilder = Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, policyName); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(errorStepKey.getPhase()); + lifecycleState.setPhaseTime(now); + lifecycleState.setAction(errorStepKey.getAction()); + lifecycleState.setActionTime(now); + lifecycleState.setStep(errorStepKey.getName()); + lifecycleState.setStepTime(now); + lifecycleState.setFailedStep(failedStepKey.getName()); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), + Collections.singletonList(policyMetadata)); + Index index = clusterState.metaData().index(indexName).getIndex(); + ClusterState nextClusterState = IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(clusterState, + indexName, () -> now, policyRegistry, false); + IndexLifecycleRunnerTests.assertClusterStateOnNextStep(clusterState, index, errorStepKey, failedStepKey, + nextClusterState, now); + LifecycleExecutionState executionState = LifecycleExecutionState.fromIndexMetadata(nextClusterState.metaData().index(indexName)); + assertThat("manual move to failed step should not count as a retry", executionState.getFailedStepRetryCount(), is(nullValue())); + } + + public void testMoveClusterStateToFailedStepWithUnknownStep() { + String indexName = "my_index"; + String policyName = "my_policy"; + long now = randomNonNegativeLong(); + Step.StepKey failedStepKey = new Step.StepKey("current_phase", MockAction.NAME, "current_step"); + Step.StepKey errorStepKey = new Step.StepKey(failedStepKey.getPhase(), failedStepKey.getAction(), ErrorStep.NAME); + + Step.StepKey registeredStepKey = new Step.StepKey(randomFrom(failedStepKey.getPhase(), "other"), + MockAction.NAME, "different_step"); + Step step = new MockStep(registeredStepKey, null); + LifecyclePolicy policy = createPolicy(policyName, failedStepKey, null); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Collections.emptyMap(), + randomNonNegativeLong(), randomNonNegativeLong()); + + PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policyName, step); + Settings.Builder indexSettingsBuilder = Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, policyName); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(errorStepKey.getPhase()); + lifecycleState.setPhaseTime(now); + lifecycleState.setAction(errorStepKey.getAction()); + lifecycleState.setActionTime(now); + lifecycleState.setStep(errorStepKey.getName()); + lifecycleState.setStepTime(now); + lifecycleState.setFailedStep(failedStepKey.getName()); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), + Collections.singletonList(policyMetadata)); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(clusterState, + indexName, () -> now, policyRegistry, false)); + assertThat(exception.getMessage(), equalTo("step [" + failedStepKey + + "] for index [my_index] with policy [my_policy] does not exist")); + } + + public void testMoveClusterStateToFailedStepIndexNotFound() { + String existingIndexName = "my_index"; + String invalidIndexName = "does_not_exist"; + ClusterState clusterState = buildClusterState(existingIndexName, Settings.builder(), LifecycleExecutionState.builder().build(), + Collections.emptyList()); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(clusterState, + invalidIndexName, () -> 0L, null, false)); + assertThat(exception.getMessage(), equalTo("index [" + invalidIndexName + "] does not exist")); + } + + public void testMoveClusterStateToFailedStepInvalidPolicySetting() { + String indexName = "my_index"; + String policyName = "my_policy"; + long now = randomNonNegativeLong(); + Step.StepKey failedStepKey = new Step.StepKey("current_phase", "current_action", "current_step"); + Step.StepKey errorStepKey = new Step.StepKey(failedStepKey.getPhase(), failedStepKey.getAction(), ErrorStep.NAME); + Step step = new MockStep(failedStepKey, null); + PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policyName, step); + Settings.Builder indexSettingsBuilder = Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, (String) null); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(errorStepKey.getPhase()); + lifecycleState.setAction(errorStepKey.getAction()); + lifecycleState.setStep(errorStepKey.getName()); + lifecycleState.setFailedStep(failedStepKey.getName()); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(clusterState, + indexName, () -> now, policyRegistry, false)); + assertThat(exception.getMessage(), equalTo("index [" + indexName + "] is not associated with an Index Lifecycle Policy")); + } + + public void testMoveClusterStateToFailedNotOnError() { + String indexName = "my_index"; + String policyName = "my_policy"; + long now = randomNonNegativeLong(); + Step.StepKey failedStepKey = new Step.StepKey("current_phase", "current_action", "current_step"); + Step step = new MockStep(failedStepKey, null); + PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policyName, step); + Settings.Builder indexSettingsBuilder = Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, (String) null); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(failedStepKey.getPhase()); + lifecycleState.setAction(failedStepKey.getAction()); + lifecycleState.setStep(failedStepKey.getName()); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(clusterState, + indexName, () -> now, policyRegistry, false)); + assertThat(exception.getMessage(), equalTo("cannot retry an action for an index [" + indexName + + "] that has not encountered an error when running a Lifecycle Policy")); + } + + public void testMoveClusterStateToPreviouslyFailedStepAsAutomaticRetry() { + String indexName = "my_index"; + String policyName = "my_policy"; + long now = randomNonNegativeLong(); + Step.StepKey failedStepKey = new Step.StepKey("current_phase", MockAction.NAME, "current_step"); + Step.StepKey errorStepKey = new Step.StepKey(failedStepKey.getPhase(), failedStepKey.getAction(), ErrorStep.NAME); + Step retryableStep = new IndexLifecycleRunnerTests.RetryableMockStep(failedStepKey, null); + LifecyclePolicy policy = createPolicy(policyName, failedStepKey, null); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Collections.emptyMap(), + randomNonNegativeLong(), randomNonNegativeLong()); + + PolicyStepsRegistry policyRegistry = createOneStepPolicyStepRegistry(policyName, retryableStep); + Settings.Builder indexSettingsBuilder = Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, policyName); + LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); + lifecycleState.setPhase(errorStepKey.getPhase()); + lifecycleState.setPhaseTime(now); + lifecycleState.setAction(errorStepKey.getAction()); + lifecycleState.setActionTime(now); + lifecycleState.setStep(errorStepKey.getName()); + lifecycleState.setStepTime(now); + lifecycleState.setFailedStep(failedStepKey.getName()); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), + Collections.singletonList(policyMetadata)); + Index index = clusterState.metaData().index(indexName).getIndex(); + ClusterState nextClusterState = IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(clusterState, + indexName, () -> now, policyRegistry, true); + IndexLifecycleRunnerTests.assertClusterStateOnNextStep(clusterState, index, errorStepKey, failedStepKey, + nextClusterState, now); + LifecycleExecutionState executionState = LifecycleExecutionState.fromIndexMetadata(nextClusterState.metaData().index(indexName)); + assertThat(executionState.getFailedStepRetryCount(), is(1)); + } + + private static LifecyclePolicy createPolicy(String policyName, Step.StepKey safeStep, Step.StepKey unsafeStep) { + Map phases = new HashMap<>(); + if (safeStep != null) { + assert MockAction.NAME.equals(safeStep.getAction()) : "The safe action needs to be MockAction.NAME"; + assert unsafeStep == null + || safeStep.getPhase().equals(unsafeStep.getPhase()) == false : "safe and unsafe actions must be in different phases"; + Map actions = new HashMap<>(); + List steps = Collections.singletonList(new MockStep(safeStep, null)); + MockAction safeAction = new MockAction(steps, true); + actions.put(safeAction.getWriteableName(), safeAction); + Phase phase = new Phase(safeStep.getPhase(), TimeValue.timeValueMillis(0), actions); + phases.put(phase.getName(), phase); + } + if (unsafeStep != null) { + assert MockAction.NAME.equals(unsafeStep.getAction()) : "The unsafe action needs to be MockAction.NAME"; + Map actions = new HashMap<>(); + List steps = Collections.singletonList(new MockStep(unsafeStep, null)); + MockAction unsafeAction = new MockAction(steps, false); + actions.put(unsafeAction.getWriteableName(), unsafeAction); + Phase phase = new Phase(unsafeStep.getPhase(), TimeValue.timeValueMillis(0), actions); + phases.put(phase.getName(), phase); + } + return newTestLifecyclePolicy(policyName, phases); + } + + private ClusterState buildClusterState(String indexName, Settings.Builder indexSettingsBuilder, + LifecycleExecutionState lifecycleState, + List lifecyclePolicyMetadatas) { + Settings indexSettings = indexSettingsBuilder.put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0).put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build(); + IndexMetaData indexMetadata = IndexMetaData.builder(indexName) + .settings(indexSettings) + .putCustom(ILM_CUSTOM_METADATA_KEY, lifecycleState.asMap()) + .build(); + + Map lifecyclePolicyMetadatasMap = lifecyclePolicyMetadatas.stream() + .collect(Collectors.toMap(LifecyclePolicyMetadata::getName, Function.identity())); + IndexLifecycleMetadata indexLifecycleMetadata = new IndexLifecycleMetadata(lifecyclePolicyMetadatasMap, OperationMode.RUNNING); + + MetaData metadata = MetaData.builder().put(indexMetadata, true).putCustom(IndexLifecycleMetadata.TYPE, indexLifecycleMetadata) + .build(); + return ClusterState.builder(new ClusterName("my_cluster")).metaData(metadata).build(); + } + + public static void assertIndexNotManagedByILM(ClusterState clusterState, Index index) { + MetaData metadata = clusterState.metaData(); + assertNotNull(metadata); + IndexMetaData indexMetadata = metadata.getIndexSafe(index); + assertNotNull(indexMetadata); + Settings indexSettings = indexMetadata.getSettings(); + assertNotNull(indexSettings); + assertFalse(LifecycleSettings.LIFECYCLE_NAME_SETTING.exists(indexSettings)); + assertFalse(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS_SETTING.exists(indexSettings)); + assertFalse(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE_SETTING.exists(indexSettings)); + } + + public static void assertClusterStateOnNextStep(ClusterState oldClusterState, Index index, Step.StepKey currentStep, + Step.StepKey nextStep, ClusterState newClusterState, long now) { + assertNotSame(oldClusterState, newClusterState); + MetaData newMetadata = newClusterState.metaData(); + assertNotSame(oldClusterState.metaData(), newMetadata); + IndexMetaData newIndexMetadata = newMetadata.getIndexSafe(index); + assertNotSame(oldClusterState.metaData().index(index), newIndexMetadata); + LifecycleExecutionState newLifecycleState = LifecycleExecutionState + .fromIndexMetadata(newClusterState.metaData().index(index)); + LifecycleExecutionState oldLifecycleState = LifecycleExecutionState + .fromIndexMetadata(oldClusterState.metaData().index(index)); + assertNotSame(oldLifecycleState, newLifecycleState); + assertEquals(nextStep.getPhase(), newLifecycleState.getPhase()); + assertEquals(nextStep.getAction(), newLifecycleState.getAction()); + assertEquals(nextStep.getName(), newLifecycleState.getStep()); + if (currentStep.getPhase().equals(nextStep.getPhase())) { + assertEquals("expected phase times to be the same but they were different", + oldLifecycleState.getPhaseTime(), newLifecycleState.getPhaseTime()); + } else { + assertEquals(now, newLifecycleState.getPhaseTime().longValue()); + } + if (currentStep.getAction().equals(nextStep.getAction())) { + assertEquals("expected action times to be the same but they were different", + oldLifecycleState.getActionTime(), newLifecycleState.getActionTime()); + } else { + assertEquals(now, newLifecycleState.getActionTime().longValue()); + } + assertEquals(now, newLifecycleState.getStepTime().longValue()); + assertEquals(null, newLifecycleState.getFailedStep()); + assertEquals(null, newLifecycleState.getStepInfo()); + } + + private IndexMetaData buildIndexMetadata(String policy, LifecycleExecutionState.Builder lifecycleState) { + return IndexMetaData.builder("index") + .settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policy)) + .numberOfShards(randomIntBetween(1, 5)) + .numberOfReplicas(randomIntBetween(0, 5)) + .putCustom(ILM_CUSTOM_METADATA_KEY, lifecycleState.build().asMap()) + .build(); + } + + private void assertClusterStateOnErrorStep(ClusterState oldClusterState, Index index, Step.StepKey currentStep, + ClusterState newClusterState, long now, String expectedCauseValue) { + assertNotSame(oldClusterState, newClusterState); + MetaData newMetadata = newClusterState.metaData(); + assertNotSame(oldClusterState.metaData(), newMetadata); + IndexMetaData newIndexMetadata = newMetadata.getIndexSafe(index); + assertNotSame(oldClusterState.metaData().index(index), newIndexMetadata); + LifecycleExecutionState newLifecycleState = LifecycleExecutionState + .fromIndexMetadata(newClusterState.metaData().index(index)); + LifecycleExecutionState oldLifecycleState = LifecycleExecutionState + .fromIndexMetadata(oldClusterState.metaData().index(index)); + assertNotSame(oldLifecycleState, newLifecycleState); + assertEquals(currentStep.getPhase(), newLifecycleState.getPhase()); + assertEquals(currentStep.getAction(), newLifecycleState.getAction()); + assertEquals(ErrorStep.NAME, newLifecycleState.getStep()); + assertEquals(currentStep.getName(), newLifecycleState.getFailedStep()); + assertThat(newLifecycleState.getStepInfo(), containsString(expectedCauseValue)); + assertEquals(oldLifecycleState.getPhaseTime(), newLifecycleState.getPhaseTime()); + assertEquals(oldLifecycleState.getActionTime(), newLifecycleState.getActionTime()); + assertEquals(now, newLifecycleState.getStepTime().longValue()); + } + + private void assertClusterStateStepInfo(ClusterState oldClusterState, Index index, Step.StepKey currentStep, + ClusterState newClusterState, ToXContentObject stepInfo) throws IOException { + XContentBuilder stepInfoXContentBuilder = JsonXContent.contentBuilder(); + stepInfo.toXContent(stepInfoXContentBuilder, ToXContent.EMPTY_PARAMS); + String expectedstepInfoValue = BytesReference.bytes(stepInfoXContentBuilder).utf8ToString(); + assertNotSame(oldClusterState, newClusterState); + MetaData newMetadata = newClusterState.metaData(); + assertNotSame(oldClusterState.metaData(), newMetadata); + IndexMetaData newIndexMetadata = newMetadata.getIndexSafe(index); + assertNotSame(oldClusterState.metaData().index(index), newIndexMetadata); + LifecycleExecutionState newLifecycleState = LifecycleExecutionState + .fromIndexMetadata(newClusterState.metaData().index(index)); + LifecycleExecutionState oldLifecycleState = LifecycleExecutionState + .fromIndexMetadata(oldClusterState.metaData().index(index)); + assertNotSame(oldLifecycleState, newLifecycleState); + assertEquals(currentStep.getPhase(), newLifecycleState.getPhase()); + assertEquals(currentStep.getAction(), newLifecycleState.getAction()); + assertEquals(currentStep.getName(), newLifecycleState.getStep()); + assertEquals(expectedstepInfoValue, newLifecycleState.getStepInfo()); + assertEquals(oldLifecycleState.getPhaseTime(), newLifecycleState.getPhaseTime()); + assertEquals(oldLifecycleState.getActionTime(), newLifecycleState.getActionTime()); + assertEquals(newLifecycleState.getStepTime(), newLifecycleState.getStepTime()); + } +} diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java index 299e4abf3058c..fa2a626b0f9bc 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java @@ -78,7 +78,7 @@ public void testExecuteSuccessfullyMoved() throws IOException { (idxMeta, stepKey) -> new MockStep(stepKey, nextStepKey)); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); - StepKey actualKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey actualKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); assertThat(actualKey, equalTo(new StepKey(currentStepKey.getPhase(), currentStepKey.getAction(), ErrorStep.NAME))); assertThat(lifecycleState.getFailedStep(), equalTo(currentStepKey.getName())); assertThat(lifecycleState.getPhaseTime(), nullValue()); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTaskTests.java index 0f8438685b39c..13b3c9eeb1c26 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTaskTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.index.Index; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; @@ -71,10 +72,10 @@ public void testExecuteSuccessfullyMoved() { AtomicBoolean changed = new AtomicBoolean(false); MoveToNextStepUpdateTask task = new MoveToNextStepUpdateTask(index, policy, currentStepKey, nextStepKey, - () -> now, state -> changed.set(true)); + () -> now, new AlwaysExistingStepRegistry(), state -> changed.set(true)); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); - StepKey actualKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey actualKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); assertThat(actualKey, equalTo(nextStepKey)); assertThat(lifecycleState.getPhaseTime(), equalTo(now)); assertThat(lifecycleState.getActionTime(), equalTo(now)); @@ -88,7 +89,8 @@ public void testExecuteDifferentCurrentStep() { StepKey notCurrentStepKey = new StepKey("not-current", "not-current", "not-current"); long now = randomNonNegativeLong(); setStateToKey(notCurrentStepKey, now); - MoveToNextStepUpdateTask task = new MoveToNextStepUpdateTask(index, policy, currentStepKey, null, () -> now, null); + MoveToNextStepUpdateTask task = new MoveToNextStepUpdateTask(index, policy, currentStepKey, null, + () -> now, new AlwaysExistingStepRegistry(), null); ClusterState newState = task.execute(clusterState); assertSame(newState, clusterState); } @@ -98,7 +100,8 @@ public void testExecuteDifferentPolicy() { long now = randomNonNegativeLong(); setStateToKey(currentStepKey, now); setStatePolicy("not-" + policy); - MoveToNextStepUpdateTask task = new MoveToNextStepUpdateTask(index, policy, currentStepKey, null, () -> now, null); + MoveToNextStepUpdateTask task = new MoveToNextStepUpdateTask(index, policy, currentStepKey, null, () -> now, + new AlwaysExistingStepRegistry(), null); ClusterState newState = task.execute(clusterState); assertSame(newState, clusterState); } @@ -113,10 +116,10 @@ public void testExecuteSuccessfulMoveWithInvalidNextStep() { SetOnce changed = new SetOnce<>(); MoveToNextStepUpdateTask task = new MoveToNextStepUpdateTask(index, policy, currentStepKey, - invalidNextStep, () -> now, s -> changed.set(true)); + invalidNextStep, () -> now, new AlwaysExistingStepRegistry(), s -> changed.set(true)); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); - StepKey actualKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey actualKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); assertThat(actualKey, equalTo(invalidNextStep)); assertThat(lifecycleState.getPhaseTime(), equalTo(now)); assertThat(lifecycleState.getActionTime(), equalTo(now)); @@ -132,7 +135,8 @@ public void testOnFailure() { setStateToKey(currentStepKey, now); - MoveToNextStepUpdateTask task = new MoveToNextStepUpdateTask(index, policy, currentStepKey, nextStepKey, () -> now, state -> {}); + MoveToNextStepUpdateTask task = new MoveToNextStepUpdateTask(index, policy, currentStepKey, nextStepKey, () -> now, + new AlwaysExistingStepRegistry(), state -> {}); Exception expectedException = new RuntimeException(); ElasticsearchException exception = expectThrows(ElasticsearchException.class, () -> task.onFailure(randomAlphaOfLength(10), expectedException)); @@ -141,6 +145,21 @@ public void testOnFailure() { assertSame(expectedException, exception.getCause()); } + /** + * Fake policy steps registry that will always pass validation that the step exists + */ + private static class AlwaysExistingStepRegistry extends PolicyStepsRegistry { + + AlwaysExistingStepRegistry() { + super(new NamedXContentRegistry(Collections.emptyList()), null); + } + + @Override + public boolean stepExists(String policy, StepKey stepKey) { + return true; + } + } + private void setStatePolicy(String policy) { clusterState = ClusterState.builder(clusterState) .metaData(MetaData.builder(clusterState.metaData()) diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/SetStepInfoUpdateTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/SetStepInfoUpdateTaskTests.java index 6224937f8dec2..d9924a2960aae 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/SetStepInfoUpdateTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/SetStepInfoUpdateTaskTests.java @@ -61,7 +61,7 @@ public void testExecuteSuccessfullySet() throws IOException { SetStepInfoUpdateTask task = new SetStepInfoUpdateTask(index, policy, currentStepKey, stepInfo); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); - StepKey actualKey = IndexLifecycleRunner.getCurrentStepKey(lifecycleState); + StepKey actualKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); assertThat(actualKey, equalTo(currentStepKey)); assertThat(lifecycleState.getPhaseTime(), nullValue()); assertThat(lifecycleState.getActionTime(), nullValue()); From 261b57c176927532f961104b70629653afe1f3b0 Mon Sep 17 00:00:00 2001 From: Rafael Acevedo Date: Fri, 6 Dec 2019 15:38:15 -0300 Subject: [PATCH 102/686] Update jackson-databind to 2.8.11.4 (#49347) --- buildSrc/version.properties | 1 + modules/ingest-geoip/build.gradle | 2 +- .../ingest-geoip/licenses/jackson-databind-2.8.11.3.jar.sha1 | 1 - .../ingest-geoip/licenses/jackson-databind-2.8.11.4.jar.sha1 | 1 + plugins/discovery-ec2/build.gradle | 2 +- .../discovery-ec2/licenses/jackson-databind-2.8.11.3.jar.sha1 | 1 - .../discovery-ec2/licenses/jackson-databind-2.8.11.4.jar.sha1 | 1 + plugins/repository-s3/build.gradle | 2 +- .../repository-s3/licenses/jackson-databind-2.8.11.3.jar.sha1 | 1 - .../repository-s3/licenses/jackson-databind-2.8.11.4.jar.sha1 | 1 + x-pack/snapshot-tool/build.gradle | 2 +- .../snapshot-tool/licenses/jackson-databind-2.8.11.3.jar.sha1 | 1 - .../snapshot-tool/licenses/jackson-databind-2.8.11.4.jar.sha1 | 1 + 13 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 modules/ingest-geoip/licenses/jackson-databind-2.8.11.3.jar.sha1 create mode 100644 modules/ingest-geoip/licenses/jackson-databind-2.8.11.4.jar.sha1 delete mode 100644 plugins/discovery-ec2/licenses/jackson-databind-2.8.11.3.jar.sha1 create mode 100644 plugins/discovery-ec2/licenses/jackson-databind-2.8.11.4.jar.sha1 delete mode 100644 plugins/repository-s3/licenses/jackson-databind-2.8.11.3.jar.sha1 create mode 100644 plugins/repository-s3/licenses/jackson-databind-2.8.11.4.jar.sha1 delete mode 100644 x-pack/snapshot-tool/licenses/jackson-databind-2.8.11.3.jar.sha1 create mode 100644 x-pack/snapshot-tool/licenses/jackson-databind-2.8.11.4.jar.sha1 diff --git a/buildSrc/version.properties b/buildSrc/version.properties index 12387d6a08440..6c7d6798a65c6 100644 --- a/buildSrc/version.properties +++ b/buildSrc/version.properties @@ -11,6 +11,7 @@ jts = 1.15.0 # you should also inspect that version to see if it can be advanced along with # the com.maxmind.geoip2:geoip2 dependency jackson = 2.8.11 +jacksondatabind = 2.8.11.4 snakeyaml = 1.17 icu4j = 62.1 supercsv = 2.4.0 diff --git a/modules/ingest-geoip/build.gradle b/modules/ingest-geoip/build.gradle index b99bb66ee8dba..6b49c26c41570 100644 --- a/modules/ingest-geoip/build.gradle +++ b/modules/ingest-geoip/build.gradle @@ -29,7 +29,7 @@ dependencies { compile('com.maxmind.geoip2:geoip2:2.9.0') // geoip2 dependencies: compile("com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}") - compile("com.fasterxml.jackson.core:jackson-databind:2.8.11.3") + compile("com.fasterxml.jackson.core:jackson-databind:${versions.jacksondatabind}") compile('com.maxmind.db:maxmind-db:1.2.2') testCompile 'org.elasticsearch:geolite2-databases:20191119' diff --git a/modules/ingest-geoip/licenses/jackson-databind-2.8.11.3.jar.sha1 b/modules/ingest-geoip/licenses/jackson-databind-2.8.11.3.jar.sha1 deleted file mode 100644 index 253a1361931c3..0000000000000 --- a/modules/ingest-geoip/licenses/jackson-databind-2.8.11.3.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -844df5aba5a1a56e00905b165b12bb34116ee858 \ No newline at end of file diff --git a/modules/ingest-geoip/licenses/jackson-databind-2.8.11.4.jar.sha1 b/modules/ingest-geoip/licenses/jackson-databind-2.8.11.4.jar.sha1 new file mode 100644 index 0000000000000..5203969bcf5c0 --- /dev/null +++ b/modules/ingest-geoip/licenses/jackson-databind-2.8.11.4.jar.sha1 @@ -0,0 +1 @@ +596d6923ff4cf7ea72ded3ac32903b9c618ce9f1 \ No newline at end of file diff --git a/plugins/discovery-ec2/build.gradle b/plugins/discovery-ec2/build.gradle index 6824353e475e2..64b38a6cac74d 100644 --- a/plugins/discovery-ec2/build.gradle +++ b/plugins/discovery-ec2/build.gradle @@ -34,7 +34,7 @@ dependencies { compile "commons-logging:commons-logging:${versions.commonslogging}" compile "org.apache.logging.log4j:log4j-1.2-api:${versions.log4j}" compile "commons-codec:commons-codec:${versions.commonscodec}" - compile 'com.fasterxml.jackson.core:jackson-databind:2.8.11.3' + compile "com.fasterxml.jackson.core:jackson-databind:${versions.jacksondatabind}" compile "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}" } diff --git a/plugins/discovery-ec2/licenses/jackson-databind-2.8.11.3.jar.sha1 b/plugins/discovery-ec2/licenses/jackson-databind-2.8.11.3.jar.sha1 deleted file mode 100644 index 253a1361931c3..0000000000000 --- a/plugins/discovery-ec2/licenses/jackson-databind-2.8.11.3.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -844df5aba5a1a56e00905b165b12bb34116ee858 \ No newline at end of file diff --git a/plugins/discovery-ec2/licenses/jackson-databind-2.8.11.4.jar.sha1 b/plugins/discovery-ec2/licenses/jackson-databind-2.8.11.4.jar.sha1 new file mode 100644 index 0000000000000..5203969bcf5c0 --- /dev/null +++ b/plugins/discovery-ec2/licenses/jackson-databind-2.8.11.4.jar.sha1 @@ -0,0 +1 @@ +596d6923ff4cf7ea72ded3ac32903b9c618ce9f1 \ No newline at end of file diff --git a/plugins/repository-s3/build.gradle b/plugins/repository-s3/build.gradle index 73ce5c2316c3b..e26fa43188b44 100644 --- a/plugins/repository-s3/build.gradle +++ b/plugins/repository-s3/build.gradle @@ -43,7 +43,7 @@ dependencies { compile "org.apache.logging.log4j:log4j-1.2-api:${versions.log4j}" compile "commons-codec:commons-codec:${versions.commonscodec}" compile "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" - compile 'com.fasterxml.jackson.core:jackson-databind:2.8.11.3' + compile "com.fasterxml.jackson.core:jackson-databind:${versions.jacksondatabind}" compile "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}" compile "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:${versions.jackson}" compile "joda-time:joda-time:${versions.joda}" diff --git a/plugins/repository-s3/licenses/jackson-databind-2.8.11.3.jar.sha1 b/plugins/repository-s3/licenses/jackson-databind-2.8.11.3.jar.sha1 deleted file mode 100644 index 253a1361931c3..0000000000000 --- a/plugins/repository-s3/licenses/jackson-databind-2.8.11.3.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -844df5aba5a1a56e00905b165b12bb34116ee858 \ No newline at end of file diff --git a/plugins/repository-s3/licenses/jackson-databind-2.8.11.4.jar.sha1 b/plugins/repository-s3/licenses/jackson-databind-2.8.11.4.jar.sha1 new file mode 100644 index 0000000000000..5203969bcf5c0 --- /dev/null +++ b/plugins/repository-s3/licenses/jackson-databind-2.8.11.4.jar.sha1 @@ -0,0 +1 @@ +596d6923ff4cf7ea72ded3ac32903b9c618ce9f1 \ No newline at end of file diff --git a/x-pack/snapshot-tool/build.gradle b/x-pack/snapshot-tool/build.gradle index 917062d54126e..5717dfd9cf230 100644 --- a/x-pack/snapshot-tool/build.gradle +++ b/x-pack/snapshot-tool/build.gradle @@ -23,7 +23,7 @@ dependencies { compile "commons-codec:commons-codec:${versions.commonscodec}" compile "org.apache.logging.log4j:log4j-1.2-api:${versions.log4j}" compile "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" - compile 'com.fasterxml.jackson.core:jackson-databind:2.8.11.3' + compile "com.fasterxml.jackson.core:jackson-databind:${versions.jacksondatabind}" compile "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}" // GCS dependencies diff --git a/x-pack/snapshot-tool/licenses/jackson-databind-2.8.11.3.jar.sha1 b/x-pack/snapshot-tool/licenses/jackson-databind-2.8.11.3.jar.sha1 deleted file mode 100644 index 253a1361931c3..0000000000000 --- a/x-pack/snapshot-tool/licenses/jackson-databind-2.8.11.3.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -844df5aba5a1a56e00905b165b12bb34116ee858 \ No newline at end of file diff --git a/x-pack/snapshot-tool/licenses/jackson-databind-2.8.11.4.jar.sha1 b/x-pack/snapshot-tool/licenses/jackson-databind-2.8.11.4.jar.sha1 new file mode 100644 index 0000000000000..5203969bcf5c0 --- /dev/null +++ b/x-pack/snapshot-tool/licenses/jackson-databind-2.8.11.4.jar.sha1 @@ -0,0 +1 @@ +596d6923ff4cf7ea72ded3ac32903b9c618ce9f1 \ No newline at end of file From f2b233cd8fa1d69b87685044dd007ea64f4b5880 Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Fri, 6 Dec 2019 13:09:44 -0700 Subject: [PATCH 103/686] Scripting: Groundwork for caching script results (#49895) In order to cache script results in the query shard cache, we need to check if scripts are deterministic. This change adds a default method to the script factories, `isResultDeterministic() -> false` which is used by the `QueryShardContext`. Script results were never cached and that does not change here. Future changes will implement this method based on whether the results of the scripts are deterministic or not and therefore cacheable. Refs: #49466 --- .../common/AnalysisPredicateScript.java | 3 +- .../PredicateTokenScriptFilterTests.java | 3 +- .../ScriptedConditionTokenFilterTests.java | 3 +- .../expression/ExpressionScriptEngine.java | 8 ++- .../script/mustache/MustacheScriptEngine.java | 8 ++- .../painless/PainlessScriptEngine.java | 27 +++++++--- .../action/PainlessExecuteAction.java | 3 +- .../painless/BaseClassTests.java | 53 ++++++++++--------- .../painless/BasicStatementTests.java | 3 +- .../elasticsearch/painless/BindingsTests.java | 3 +- .../elasticsearch/painless/FactoryTests.java | 7 +-- .../expertscript/ExpertScriptPlugin.java | 47 ++++++++++++---- .../index/query/InnerHitContextBuilder.java | 2 +- .../index/query/IntervalFilterScript.java | 3 +- .../index/query/IntervalsSourceProvider.java | 2 +- .../index/query/QueryShardContext.java | 14 +++-- .../index/query/ScriptQueryBuilder.java | 2 +- .../index/query/TermsSetQueryBuilder.java | 4 +- .../ScriptScoreFunctionBuilder.java | 2 +- .../ScriptScoreQueryBuilder.java | 2 +- .../script/AggregationScript.java | 2 +- .../script/BucketAggregationScript.java | 2 +- .../BucketAggregationSelectorScript.java | 2 +- .../org/elasticsearch/script/FieldScript.java | 2 +- .../elasticsearch/script/FilterScript.java | 2 +- .../script/IngestConditionalScript.java | 2 +- .../elasticsearch/script/IngestScript.java | 2 +- .../script/NumberSortScript.java | 2 +- .../org/elasticsearch/script/ScoreScript.java | 2 +- .../elasticsearch/script/ScriptContext.java | 2 +- .../elasticsearch/script/ScriptEngine.java | 7 ++- .../elasticsearch/script/ScriptFactory.java | 28 ++++++++++ .../elasticsearch/script/ScriptService.java | 2 +- .../script/ScriptedMetricAggContexts.java | 8 +-- .../SignificantTermsHeuristicScoreScript.java | 2 +- .../script/SimilarityScript.java | 4 +- .../script/SimilarityWeightScript.java | 2 +- .../script/StringSortScript.java | 2 +- .../elasticsearch/script/TemplateScript.java | 2 +- .../script/TermsSetQueryScript.java | 2 +- .../elasticsearch/script/UpdateScript.java | 2 +- .../heuristics/ScriptHeuristic.java | 2 +- .../ScriptedMetricAggregationBuilder.java | 6 +-- .../metrics/TopHitsAggregationBuilder.java | 2 +- .../pipeline/MovingFunctionScript.java | 3 +- .../support/ValuesSourceConfig.java | 2 +- .../search/sort/ScriptSortBuilder.java | 4 +- .../phrase/PhraseSuggestionBuilder.java | 2 +- .../query/IntervalQueryBuilderTests.java | 3 +- .../script/ScriptContextTests.java | 12 ++--- .../script/ScriptLanguagesInfoTests.java | 2 +- .../functionscore/ExplainableScriptIT.java | 8 ++- .../search/suggest/SuggestSearchIT.java | 8 ++- .../ingest/TestTemplateService.java | 3 +- .../script/MockScriptEngine.java | 2 +- .../script/MockMustacheScriptEngine.java | 2 +- .../test/MockPainlessScriptEngine.java | 3 +- .../condition/WatcherConditionScript.java | 3 +- .../script/WatcherTransformScript.java | 3 +- 59 files changed, 236 insertions(+), 114 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/script/ScriptFactory.java diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java index 5d8c491efc585..f14bd9ded9cc1 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java @@ -27,6 +27,7 @@ import org.apache.lucene.analysis.tokenattributes.TypeAttribute; import org.apache.lucene.util.AttributeSource; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; /** * A predicate based on the current token in a TokenStream @@ -107,7 +108,7 @@ public boolean isKeyword() { */ public abstract boolean execute(Token token); - public interface Factory { + public interface Factory extends ScriptFactory { AnalysisPredicateScript newInstance(); } diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java index 84ba5e5d3373c..9e61a59237348 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.indices.analysis.AnalysisModule; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTokenStreamTestCase; import org.elasticsearch.test.IndexSettingsModule; @@ -63,7 +64,7 @@ public boolean execute(Token token) { @SuppressWarnings("unchecked") ScriptService scriptService = new ScriptService(indexSettings, Collections.emptyMap(), Collections.emptyMap()){ @Override - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { assertEquals(context, AnalysisPredicateScript.CONTEXT); assertEquals(new Script("my_script"), script); return (FactoryType) factory; diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterTests.java index 58226ac169bc3..d2bbc780c8747 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.indices.analysis.AnalysisModule; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTokenStreamTestCase; import org.elasticsearch.test.IndexSettingsModule; @@ -63,7 +64,7 @@ public boolean execute(Token token) { @SuppressWarnings("unchecked") ScriptService scriptService = new ScriptService(indexSettings, Collections.emptyMap(), Collections.emptyMap()){ @Override - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { assertEquals(context, AnalysisPredicateScript.CONTEXT); assertEquals(new Script("token.getPosition() > 1"), script); return (FactoryType) factory; diff --git a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java index 3df42b53cbb34..ead6d733b10de 100644 --- a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java +++ b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java @@ -44,6 +44,7 @@ import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; import org.elasticsearch.script.ScriptException; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.TermsSetQueryScript; import org.elasticsearch.search.lookup.SearchLookup; @@ -108,7 +109,12 @@ public String getType() { } @Override - public T compile(String scriptName, String scriptSource, ScriptContext context, Map params) { + public T compile( + String scriptName, + String scriptSource, + ScriptContext context, + Map params + ) { // classloader created here final SecurityManager sm = System.getSecurityManager(); SpecialPermission.check(); diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java index f453905089fdd..f14dd35d339fd 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java @@ -32,6 +32,7 @@ import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; import org.elasticsearch.script.ScriptException; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.TemplateScript; import java.io.Reader; @@ -64,7 +65,12 @@ public final class MustacheScriptEngine implements ScriptEngine { * @return a compiled template object for later execution. * */ @Override - public T compile(String templateName, String templateSource, ScriptContext context, Map options) { + public T compile( + String templateName, + String templateSource, + ScriptContext context, + Map options + ) { if (context.instanceClazz.equals(TemplateScript.class) == false) { throw new IllegalArgumentException("mustache engine does not know how to handle context [" + context.name + "]"); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java index 7f64e992bc122..91448a4a3788e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java @@ -28,6 +28,7 @@ import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; import org.elasticsearch.script.ScriptException; +import org.elasticsearch.script.ScriptFactory; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -66,7 +67,7 @@ public final class PainlessScriptEngine implements ScriptEngine { */ private static final AccessControlContext COMPILATION_CONTEXT; - /** + /* * Setup the allowed permissions. */ static { @@ -122,7 +123,12 @@ public String getType() { } @Override - public T compile(String scriptName, String scriptSource, ScriptContext context, Map params) { + public T compile( + String scriptName, + String scriptSource, + ScriptContext context, + Map params + ) { Compiler compiler = contextsToCompilers.get(context); // Check we ourselves are not being called by unprivileged code. @@ -162,12 +168,16 @@ public Set> getSupportedContexts() { * @param The factory class. * @return A factory class that will return script instances. */ - private Type generateStatefulFactory(Loader loader, ScriptContext context, Set extractedVariables) { + private Type generateStatefulFactory( + Loader loader, + ScriptContext context, + Set extractedVariables + ) { int classFrames = ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS; int classAccess = Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL; String interfaceBase = Type.getType(context.statefulFactoryClazz).getInternalName(); String className = interfaceBase + "$StatefulFactory"; - String classInterfaces[] = new String[] { interfaceBase }; + String[] classInterfaces = new String[] { interfaceBase }; ClassWriter writer = new ClassWriter(classFrames); writer.visit(WriterConstants.CLASS_VERSION, classAccess, className, null, OBJECT_TYPE.getInternalName(), classInterfaces); @@ -263,12 +273,17 @@ private Type generateStatefulFactory(Loader loader, ScriptContext context * @param The factory class. * @return A factory class that will return script instances. */ - private T generateFactory(Loader loader, ScriptContext context, Set extractedVariables, Type classType) { + private T generateFactory( + Loader loader, + ScriptContext context, + Set extractedVariables, + Type classType + ) { int classFrames = ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS; int classAccess = Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER| Opcodes.ACC_FINAL; String interfaceBase = Type.getType(context.factoryClazz).getInternalName(); String className = interfaceBase + "$Factory"; - String classInterfaces[] = new String[] { interfaceBase }; + String[] classInterfaces = new String[] { interfaceBase }; ClassWriter writer = new ClassWriter(classFrames); writer.visit(WriterConstants.CLASS_VERSION, classAccess, className, null, OBJECT_TYPE.getInternalName(), classInterfaces); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java index b6bb1a842640d..e38d87e69ccb3 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java @@ -75,6 +75,7 @@ import org.elasticsearch.script.ScoreScript; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; import org.elasticsearch.threadpool.ThreadPool; @@ -415,7 +416,7 @@ public Map getParams() { public abstract Object execute(); - public interface Factory { + public interface Factory extends ScriptFactory { PainlessTestScript newInstance(Map params); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BaseClassTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BaseClassTests.java index ae96c8b3b7944..96790301139f3 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BaseClassTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BaseClassTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.painless.spi.Whitelist; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import java.util.Collections; import java.util.HashMap; @@ -65,7 +66,7 @@ protected Map, List> scriptContexts() { public abstract static class Gets { - public interface Factory { + public interface Factory extends ScriptFactory { Gets newInstance(String testString, int testInt, Map params); } @@ -111,7 +112,7 @@ public void testGets() throws Exception { } public abstract static class NoArgs { - public interface Factory { + public interface Factory extends ScriptFactory { NoArgs newInstance(); } @@ -137,7 +138,7 @@ public void testNoArgs() throws Exception { } public abstract static class OneArg { - public interface Factory { + public interface Factory extends ScriptFactory { OneArg newInstance(); } @@ -154,7 +155,7 @@ public void testOneArg() throws Exception { } public abstract static class ArrayArg { - public interface Factory { + public interface Factory extends ScriptFactory { ArrayArg newInstance(); } @@ -171,7 +172,7 @@ public void testArrayArg() throws Exception { } public abstract static class PrimitiveArrayArg { - public interface Factory { + public interface Factory extends ScriptFactory { PrimitiveArrayArg newInstance(); } @@ -188,7 +189,7 @@ public void testPrimitiveArrayArg() throws Exception { } public abstract static class DefArrayArg { - public interface Factory { + public interface Factory extends ScriptFactory { DefArrayArg newInstance(); } @@ -212,7 +213,7 @@ public void testDefArrayArg()throws Exception { } public abstract static class ManyArgs { - public interface Factory { + public interface Factory extends ScriptFactory { ManyArgs newInstance(); } @@ -251,7 +252,7 @@ public void testManyArgs() throws Exception { } public abstract static class VarArgs { - public interface Factory { + public interface Factory extends ScriptFactory { VarArgs newInstance(); } @@ -267,7 +268,7 @@ public void testVarArgs() throws Exception { } public abstract static class DefaultMethods { - public interface Factory { + public interface Factory extends ScriptFactory { DefaultMethods newInstance(); } @@ -301,7 +302,7 @@ public void testDefaultMethods() throws Exception { } public abstract static class ReturnsVoid { - public interface Factory { + public interface Factory extends ScriptFactory { ReturnsVoid newInstance(); } @@ -325,7 +326,7 @@ public void testReturnsVoid() throws Exception { } public abstract static class ReturnsPrimitiveBoolean { - public interface Factory { + public interface Factory extends ScriptFactory { ReturnsPrimitiveBoolean newInstance(); } @@ -391,20 +392,20 @@ public void testReturnsPrimitiveBoolean() throws Exception { } public abstract static class ReturnsPrimitiveInt { - public interface Factory { + public interface Factory extends ScriptFactory { ReturnsPrimitiveInt newInstance(); } public static final ScriptContext CONTEXT = new ScriptContext<>("returnsprimitiveint", Factory.class); - + public static final String[] PARAMETERS = new String[] {}; public abstract int execute(); } public void testReturnsPrimitiveInt() throws Exception { - assertEquals(1, + assertEquals(1, scriptEngine.compile("testReturnsPrimitiveInt0", "1", ReturnsPrimitiveInt.CONTEXT, emptyMap()) .newInstance().execute()); - assertEquals(1, + assertEquals(1, scriptEngine.compile("testReturnsPrimitiveInt1", "(int) 1L", ReturnsPrimitiveInt.CONTEXT, emptyMap()) .newInstance().execute()); assertEquals(1, scriptEngine.compile("testReturnsPrimitiveInt2", "(int) 1.1d", ReturnsPrimitiveInt.CONTEXT, emptyMap()) @@ -455,12 +456,12 @@ public void testReturnsPrimitiveInt() throws Exception { } public abstract static class ReturnsPrimitiveFloat { - public interface Factory { + public interface Factory extends ScriptFactory { ReturnsPrimitiveFloat newInstance(); } public static final ScriptContext CONTEXT = new ScriptContext<>("returnsprimitivefloat", Factory.class); - + public static final String[] PARAMETERS = new String[] {}; public abstract float execute(); } @@ -504,12 +505,12 @@ public void testReturnsPrimitiveFloat() throws Exception { } public abstract static class ReturnsPrimitiveDouble { - public interface Factory { + public interface Factory extends ScriptFactory { ReturnsPrimitiveDouble newInstance(); } public static final ScriptContext CONTEXT = new ScriptContext<>("returnsprimitivedouble", Factory.class); - + public static final String[] PARAMETERS = new String[] {}; public abstract double execute(); } @@ -567,7 +568,7 @@ public void testReturnsPrimitiveDouble() throws Exception { } public abstract static class NoArgsConstant { - public interface Factory { + public interface Factory extends ScriptFactory { NoArgsConstant newInstance(); } @@ -584,7 +585,7 @@ public void testNoArgsConstant() { } public abstract static class WrongArgsConstant { - public interface Factory { + public interface Factory extends ScriptFactory { WrongArgsConstant newInstance(); } @@ -602,7 +603,7 @@ public void testWrongArgsConstant() { } public abstract static class WrongLengthOfArgConstant { - public interface Factory { + public interface Factory extends ScriptFactory { WrongLengthOfArgConstant newInstance(); } @@ -619,7 +620,7 @@ public void testWrongLengthOfArgConstant() { } public abstract static class UnknownArgType { - public interface Factory { + public interface Factory extends ScriptFactory { UnknownArgType newInstance(); } @@ -636,7 +637,7 @@ public void testUnknownArgType() { } public abstract static class UnknownReturnType { - public interface Factory { + public interface Factory extends ScriptFactory { UnknownReturnType newInstance(); } @@ -653,7 +654,7 @@ public void testUnknownReturnType() { } public abstract static class UnknownArgTypeInArray { - public interface Factory { + public interface Factory extends ScriptFactory { UnknownArgTypeInArray newInstance(); } @@ -670,7 +671,7 @@ public void testUnknownArgTypeInArray() { } public abstract static class TwoExecuteMethods { - public interface Factory { + public interface Factory extends ScriptFactory { TwoExecuteMethods newInstance(); } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java index e4d1db2243b82..72fc01dbf2ce6 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java @@ -2,6 +2,7 @@ import org.elasticsearch.painless.spi.Whitelist; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import java.util.ArrayList; import java.util.Collections; @@ -260,7 +261,7 @@ public void testReturnStatement() { } public abstract static class OneArg { - public interface Factory { + public interface Factory extends ScriptFactory { OneArg newInstance(); } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java index 171880abd7907..d165e0ef76d81 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.painless.spi.WhitelistInstanceBinding; import org.elasticsearch.painless.spi.WhitelistLoader; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import java.util.ArrayList; import java.util.Collections; @@ -89,7 +90,7 @@ public abstract static class BindingsTestScript { public static final String[] PARAMETERS = { "test", "bound" }; public int getTestValue() {return 7;} public abstract int execute(int test, int bound); - public interface Factory { + public interface Factory extends ScriptFactory { BindingsTestScript newInstance(); } public static final ScriptContext CONTEXT = new ScriptContext<>("bindings_test", Factory.class); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java index 556ef8dd3c6d3..1d3fd829d2512 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.painless.spi.Whitelist; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.TemplateScript; import java.util.Collections; @@ -84,7 +85,7 @@ public interface StatefulFactory { boolean needsD(); } - public interface Factory { + public interface Factory extends ScriptFactory { StatefulFactory newFactory(int x, int y); boolean needsTest(); @@ -137,7 +138,7 @@ public Map getParams() { public static final String[] PARAMETERS = new String[] {"test"}; public abstract Object execute(int test); - public interface Factory { + public interface Factory extends ScriptFactory { FactoryTestScript newInstance(Map params); boolean needsTest(); @@ -165,7 +166,7 @@ public abstract static class EmptyTestScript { public static final String[] PARAMETERS = {}; public abstract Object execute(); - public interface Factory { + public interface Factory extends ScriptFactory { EmptyTestScript newInstance(); } diff --git a/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java b/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java index 5259d32a2837b..5846f000f5e2d 100644 --- a/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java +++ b/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java @@ -29,6 +29,7 @@ import org.elasticsearch.script.ScoreScript.LeafFactory; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.search.lookup.SearchLookup; import java.io.IOException; @@ -38,16 +39,21 @@ import java.util.Set; /** - * An example script plugin that adds a {@link ScriptEngine} implementing expert scoring. + * An example script plugin that adds a {@link ScriptEngine} + * implementing expert scoring. */ public class ExpertScriptPlugin extends Plugin implements ScriptPlugin { @Override - public ScriptEngine getScriptEngine(Settings settings, Collection> contexts) { + public ScriptEngine getScriptEngine( + Settings settings, + Collection> contexts + ) { return new MyExpertScriptEngine(); } - /** An example {@link ScriptEngine} that uses Lucene segment details to implement pure document frequency scoring. */ + /** An example {@link ScriptEngine} that uses Lucene segment details to + * implement pure document frequency scoring. */ // tag::expert_engine private static class MyExpertScriptEngine implements ScriptEngine { @Override @@ -56,8 +62,12 @@ public String getType() { } @Override - public T compile(String scriptName, String scriptSource, - ScriptContext context, Map params) { + public T compile( + String scriptName, + String scriptSource, + ScriptContext context, + Map params + ) { if (context.equals(ScoreScript.CONTEXT) == false) { throw new IllegalArgumentException(getType() + " scripts cannot be used for context [" @@ -65,7 +75,7 @@ public T compile(String scriptName, String scriptSource, } // we use the script "source" as the script identifier if ("pure_df".equals(scriptSource)) { - ScoreScript.Factory factory = PureDfLeafFactory::new; + ScoreScript.Factory factory = new PureDfFactory(); return context.factoryClazz.cast(factory); } throw new IllegalArgumentException("Unknown script name " @@ -82,6 +92,23 @@ public Set> getSupportedContexts() { return Set.of(ScoreScript.CONTEXT); } + private static class PureDfFactory implements ScoreScript.Factory { + @Override + public boolean isResultDeterministic() { + // PureDfLeafFactory only uses deterministic APIs, this + // implies the results are cacheable. + return true; + } + + @Override + public LeafFactory newFactory( + Map params, + SearchLookup lookup + ) { + return new PureDfLeafFactory(params, lookup); + } + } + private static class PureDfLeafFactory implements LeafFactory { private final Map params; private final SearchLookup lookup; @@ -121,7 +148,9 @@ public ScoreScript newInstance(LeafReaderContext context) */ return new ScoreScript(params, lookup, context) { @Override - public double execute(ExplanationHolder explanation) { + public double execute( + ExplanationHolder explanation + ) { return 0.0d; } }; @@ -147,8 +176,8 @@ public void setDocument(int docid) { public double execute(ExplanationHolder explanation) { if (postings.docID() != currentDocid) { /* - * advance moved past the current doc, so this doc - * has no occurrences of the term + * advance moved past the current doc, so this + * doc has no occurrences of the term */ return 0.0d; } diff --git a/server/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java b/server/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java index 23cb8ab955370..845a3539e9e85 100644 --- a/server/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java @@ -93,7 +93,7 @@ protected void setupInnerHitsContext(QueryShardContext queryShardContext, if (innerHitBuilder.getScriptFields() != null) { for (SearchSourceBuilder.ScriptField field : innerHitBuilder.getScriptFields()) { QueryShardContext innerContext = innerHitsContext.getQueryShardContext(); - FieldScript.Factory factory = innerContext.getScriptService().compile(field.script(), FieldScript.CONTEXT); + FieldScript.Factory factory = innerContext.compile(field.script(), FieldScript.CONTEXT); FieldScript.LeafFactory fieldScript = factory.newFactory(field.script().getParams(), innerHitsContext.lookup()); innerHitsContext.scriptFields().add(new org.elasticsearch.search.fetch.subphase.ScriptFieldsContext.ScriptField( field.fieldName(), fieldScript, field.ignoreFailure())); diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalFilterScript.java b/server/src/main/java/org/elasticsearch/index/query/IntervalFilterScript.java index 1f86179dca73c..2d0fc99258c4d 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IntervalFilterScript.java +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalFilterScript.java @@ -21,6 +21,7 @@ import org.apache.lucene.queries.intervals.IntervalIterator; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; /** * Base class for scripts used as interval filters, see {@link IntervalsSourceProvider.IntervalFilter} @@ -50,7 +51,7 @@ public int getGaps() { public abstract boolean execute(Interval interval); - public interface Factory { + public interface Factory extends ScriptFactory { IntervalFilterScript newInstance(); } diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java index 97093f0c92fc9..167266e0892fd 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java @@ -717,7 +717,7 @@ public IntervalFilter(StreamInput in) throws IOException { public IntervalsSource filter(IntervalsSource input, QueryShardContext context, MappedFieldType fieldType) throws IOException { if (script != null) { - IntervalFilterScript ifs = context.getScriptService().compile(script, IntervalFilterScript.CONTEXT).newInstance(); + IntervalFilterScript ifs = context.compile(script, IntervalFilterScript.CONTEXT).newInstance(); return new ScriptFilterSource(input, script.getIdOrCode(), ifs); } IntervalsSource filterSource = filter.getSource(context, fieldType); diff --git a/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java b/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java index c599fbcb4274e..88a8351790c0a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java @@ -52,6 +52,9 @@ import org.elasticsearch.index.mapper.TypeFieldMapper; import org.elasticsearch.index.query.support.NestedScope; import org.elasticsearch.index.similarity.SimilarityService; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.transport.RemoteClusterAware; @@ -319,10 +322,13 @@ public Index index() { return indexSettings.getIndex(); } - /** Return the script service to allow compiling scripts. */ - public final ScriptService getScriptService() { - failIfFrozen(); - return scriptService; + /** Compile script using script service */ + public FactoryType compile(Script script, ScriptContext context) { + FactoryType factory = scriptService.compile(script, context); + if (factory.isResultDeterministic() == false) { + failIfFrozen(); + } + return factory; } /** diff --git a/server/src/main/java/org/elasticsearch/index/query/ScriptQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/ScriptQueryBuilder.java index 8a3666afb9d12..e9b18bd0aa1f1 100644 --- a/server/src/main/java/org/elasticsearch/index/query/ScriptQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/ScriptQueryBuilder.java @@ -130,7 +130,7 @@ public static ScriptQueryBuilder fromXContent(XContentParser parser) throws IOEx @Override protected Query doToQuery(QueryShardContext context) throws IOException { - FilterScript.Factory factory = context.getScriptService().compile(script, FilterScript.CONTEXT); + FilterScript.Factory factory = context.compile(script, FilterScript.CONTEXT); FilterScript.LeafFactory filterScript = factory.newFactory(script.getParams(), context.lookup()); return new ScriptQuery(script, filterScript); } diff --git a/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java index 1e151896df046..ac2526d34325a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java @@ -40,6 +40,7 @@ import org.elasticsearch.index.fielddata.IndexNumericFieldData; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.script.Script; +import org.elasticsearch.script.TermsSetQueryScript; import java.io.IOException; import java.util.ArrayList; @@ -47,7 +48,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import org.elasticsearch.script.TermsSetQueryScript; public final class TermsSetQueryBuilder extends AbstractQueryBuilder { @@ -262,7 +262,7 @@ private LongValuesSource createValuesSource(QueryShardContext context) { IndexNumericFieldData fieldData = context.getForField(msmFieldType); longValuesSource = new FieldValuesSource(fieldData); } else if (minimumShouldMatchScript != null) { - TermsSetQueryScript.Factory factory = context.getScriptService().compile(minimumShouldMatchScript, + TermsSetQueryScript.Factory factory = context.compile(minimumShouldMatchScript, TermsSetQueryScript.CONTEXT); Map params = new HashMap<>(); params.putAll(minimumShouldMatchScript.getParams()); diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreFunctionBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreFunctionBuilder.java index 8fc2d4ff6b1a4..3d5326d5f3da0 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreFunctionBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreFunctionBuilder.java @@ -92,7 +92,7 @@ protected int doHashCode() { @Override protected ScoreFunction doToFunction(QueryShardContext context) { try { - ScoreScript.Factory factory = context.getScriptService().compile(script, ScoreScript.CONTEXT); + ScoreScript.Factory factory = context.compile(script, ScoreScript.CONTEXT); ScoreScript.LeafFactory searchScript = factory.newFactory(script.getParams(), context.lookup()); return new ScriptScoreFunction(script, searchScript, context.index().getName(), context.getShardId(), context.indexVersionCreated()); diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreQueryBuilder.java index 59086f163a17f..e55c6d318777a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreQueryBuilder.java @@ -170,7 +170,7 @@ protected int doHashCode() { @Override protected Query doToQuery(QueryShardContext context) throws IOException { - ScoreScript.Factory factory = context.getScriptService().compile(script, ScoreScript.CONTEXT); + ScoreScript.Factory factory = context.compile(script, ScoreScript.CONTEXT); ScoreScript.LeafFactory scoreScriptFactory = factory.newFactory(script.getParams(), context.lookup()); Query query = this.query.toQuery(context); return new ScriptScoreQuery(query, script, scoreScriptFactory, minScore, diff --git a/server/src/main/java/org/elasticsearch/script/AggregationScript.java b/server/src/main/java/org/elasticsearch/script/AggregationScript.java index d6ef2e7b14be2..4d264445d25f0 100644 --- a/server/src/main/java/org/elasticsearch/script/AggregationScript.java +++ b/server/src/main/java/org/elasticsearch/script/AggregationScript.java @@ -150,7 +150,7 @@ public interface LeafFactory { /** * A factory to construct stateful {@link AggregationScript} factories for a specific index. */ - public interface Factory { + public interface Factory extends ScriptFactory { LeafFactory newFactory(Map params, SearchLookup lookup); } } diff --git a/server/src/main/java/org/elasticsearch/script/BucketAggregationScript.java b/server/src/main/java/org/elasticsearch/script/BucketAggregationScript.java index 76ff776353ef2..897134173537a 100644 --- a/server/src/main/java/org/elasticsearch/script/BucketAggregationScript.java +++ b/server/src/main/java/org/elasticsearch/script/BucketAggregationScript.java @@ -48,7 +48,7 @@ public Map getParams() { public abstract Number execute(); - public interface Factory { + public interface Factory extends ScriptFactory { BucketAggregationScript newInstance(Map params); } } diff --git a/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java b/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java index a8e2fad7cdcda..3c765439223d2 100644 --- a/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java +++ b/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java @@ -48,7 +48,7 @@ public Map getParams() { public abstract boolean execute(); - public interface Factory { + public interface Factory extends ScriptFactory { BucketAggregationSelectorScript newInstance(Map params); } } diff --git a/server/src/main/java/org/elasticsearch/script/FieldScript.java b/server/src/main/java/org/elasticsearch/script/FieldScript.java index 806c5b92cb9e3..06666368d5ade 100644 --- a/server/src/main/java/org/elasticsearch/script/FieldScript.java +++ b/server/src/main/java/org/elasticsearch/script/FieldScript.java @@ -87,7 +87,7 @@ public interface LeafFactory { FieldScript newInstance(LeafReaderContext ctx) throws IOException; } - public interface Factory { + public interface Factory extends ScriptFactory { LeafFactory newFactory(Map params, SearchLookup lookup); } diff --git a/server/src/main/java/org/elasticsearch/script/FilterScript.java b/server/src/main/java/org/elasticsearch/script/FilterScript.java index 500ab99c6a56b..166a2b59ffa3f 100644 --- a/server/src/main/java/org/elasticsearch/script/FilterScript.java +++ b/server/src/main/java/org/elasticsearch/script/FilterScript.java @@ -70,7 +70,7 @@ public interface LeafFactory { } /** A factory to construct stateful {@link FilterScript} factories for a specific index. */ - public interface Factory { + public interface Factory extends ScriptFactory { LeafFactory newFactory(Map params, SearchLookup lookup); } diff --git a/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java b/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java index 27ce29b95dc50..44d87cfe6aba2 100644 --- a/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java +++ b/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java @@ -45,7 +45,7 @@ public Map getParams() { public abstract boolean execute(Map ctx); - public interface Factory { + public interface Factory extends ScriptFactory { IngestConditionalScript newInstance(Map params); } } diff --git a/server/src/main/java/org/elasticsearch/script/IngestScript.java b/server/src/main/java/org/elasticsearch/script/IngestScript.java index f357394ed31f0..7104ed7d9b0d2 100644 --- a/server/src/main/java/org/elasticsearch/script/IngestScript.java +++ b/server/src/main/java/org/elasticsearch/script/IngestScript.java @@ -46,7 +46,7 @@ public Map getParams() { public abstract void execute(Map ctx); - public interface Factory { + public interface Factory extends ScriptFactory { IngestScript newInstance(Map params); } } diff --git a/server/src/main/java/org/elasticsearch/script/NumberSortScript.java b/server/src/main/java/org/elasticsearch/script/NumberSortScript.java index d0b3fdbed363e..f1eb118b8bdd5 100644 --- a/server/src/main/java/org/elasticsearch/script/NumberSortScript.java +++ b/server/src/main/java/org/elasticsearch/script/NumberSortScript.java @@ -54,7 +54,7 @@ public interface LeafFactory { /** * A factory to construct stateful {@link NumberSortScript} factories for a specific index. */ - public interface Factory { + public interface Factory extends ScriptFactory { LeafFactory newFactory(Map params, SearchLookup lookup); } } diff --git a/server/src/main/java/org/elasticsearch/script/ScoreScript.java b/server/src/main/java/org/elasticsearch/script/ScoreScript.java index 7c2c09d17afe7..9f535e898b00c 100644 --- a/server/src/main/java/org/elasticsearch/script/ScoreScript.java +++ b/server/src/main/java/org/elasticsearch/script/ScoreScript.java @@ -229,7 +229,7 @@ public interface LeafFactory { } /** A factory to construct stateful {@link ScoreScript} factories for a specific index. */ - public interface Factory { + public interface Factory extends ScriptFactory { ScoreScript.LeafFactory newFactory(Map params, SearchLookup lookup); diff --git a/server/src/main/java/org/elasticsearch/script/ScriptContext.java b/server/src/main/java/org/elasticsearch/script/ScriptContext.java index 081a26d1e511a..4e927da09d118 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptContext.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptContext.java @@ -54,7 +54,7 @@ * If the variable name starts with an underscore, for example, {@code _score}, the needs method would * be {@code boolean needs_score()}. */ -public final class ScriptContext { +public final class ScriptContext { /** A unique identifier for this context. */ public final String name; diff --git a/server/src/main/java/org/elasticsearch/script/ScriptEngine.java b/server/src/main/java/org/elasticsearch/script/ScriptEngine.java index 9ace06d701d14..4c38ae5c6e19c 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptEngine.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptEngine.java @@ -42,7 +42,12 @@ public interface ScriptEngine extends Closeable { * @param params compile-time parameters (such as flags to the compiler) * @return A compiled script of the FactoryType from {@link ScriptContext} */ - FactoryType compile(String name, String code, ScriptContext context, Map params); + FactoryType compile( + String name, + String code, + ScriptContext context, + Map params + ); @Override default void close() throws IOException {} diff --git a/server/src/main/java/org/elasticsearch/script/ScriptFactory.java b/server/src/main/java/org/elasticsearch/script/ScriptFactory.java new file mode 100644 index 0000000000000..d05e4f77c6449 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/ScriptFactory.java @@ -0,0 +1,28 @@ +/* + * 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. + */ + +package org.elasticsearch.script; + +/** + * Contains utility methods for compiled scripts without impacting concrete script signatures + */ +public interface ScriptFactory { + /** Returns {@code true} if the result of the script will be deterministic, {@code false} otherwise. */ + default boolean isResultDeterministic() { return false; } +} diff --git a/server/src/main/java/org/elasticsearch/script/ScriptService.java b/server/src/main/java/org/elasticsearch/script/ScriptService.java index a1788ee74a163..77375838e7f16 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptService.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptService.java @@ -284,7 +284,7 @@ void setMaxCompilationRate(Tuple newRate) { * * @return a compiled script which may be used to construct instances of a script for the given context */ - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { Objects.requireNonNull(script); Objects.requireNonNull(context); diff --git a/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java b/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java index 53c1aa3da1bd4..782c4251c4c84 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java @@ -52,7 +52,7 @@ public Object getState() { public abstract void execute(); - public interface Factory { + public interface Factory extends ScriptFactory { InitScript newInstance(Map params, Map state); } @@ -129,7 +129,7 @@ public interface LeafFactory { MapScript newInstance(LeafReaderContext ctx); } - public interface Factory { + public interface Factory extends ScriptFactory { LeafFactory newFactory(Map params, Map state, SearchLookup lookup); } @@ -156,7 +156,7 @@ public Map getState() { public abstract Object execute(); - public interface Factory { + public interface Factory extends ScriptFactory { CombineScript newInstance(Map params, Map state); } @@ -183,7 +183,7 @@ public List getStates() { public abstract Object execute(); - public interface Factory { + public interface Factory extends ScriptFactory { ReduceScript newInstance(Map params, List states); } diff --git a/server/src/main/java/org/elasticsearch/script/SignificantTermsHeuristicScoreScript.java b/server/src/main/java/org/elasticsearch/script/SignificantTermsHeuristicScoreScript.java index 0296bc36ce107..b1a280783a70d 100644 --- a/server/src/main/java/org/elasticsearch/script/SignificantTermsHeuristicScoreScript.java +++ b/server/src/main/java/org/elasticsearch/script/SignificantTermsHeuristicScoreScript.java @@ -32,7 +32,7 @@ public abstract class SignificantTermsHeuristicScoreScript { public abstract double execute(Map params); - public interface Factory { + public interface Factory extends ScriptFactory { SignificantTermsHeuristicScoreScript newInstance(); } } diff --git a/server/src/main/java/org/elasticsearch/script/SimilarityScript.java b/server/src/main/java/org/elasticsearch/script/SimilarityScript.java index 4aeb4063959b3..c3efb55a6031e 100644 --- a/server/src/main/java/org/elasticsearch/script/SimilarityScript.java +++ b/server/src/main/java/org/elasticsearch/script/SimilarityScript.java @@ -22,7 +22,7 @@ import org.elasticsearch.index.similarity.ScriptedSimilarity; /** A script that is used to build {@link ScriptedSimilarity} instances. */ -public abstract class SimilarityScript { +public abstract class SimilarityScript { /** Compute the score. * @param weight weight computed by the {@link SimilarityWeightScript} if any, or 1. @@ -34,7 +34,7 @@ public abstract class SimilarityScript { public abstract double execute(double weight, ScriptedSimilarity.Query query, ScriptedSimilarity.Field field, ScriptedSimilarity.Term term, ScriptedSimilarity.Doc doc); - public interface Factory { + public interface Factory extends ScriptFactory { SimilarityScript newInstance(); } diff --git a/server/src/main/java/org/elasticsearch/script/SimilarityWeightScript.java b/server/src/main/java/org/elasticsearch/script/SimilarityWeightScript.java index 04bbc3cccf40a..2797da64a0e25 100644 --- a/server/src/main/java/org/elasticsearch/script/SimilarityWeightScript.java +++ b/server/src/main/java/org/elasticsearch/script/SimilarityWeightScript.java @@ -32,7 +32,7 @@ public abstract class SimilarityWeightScript { public abstract double execute(ScriptedSimilarity.Query query, ScriptedSimilarity.Field field, ScriptedSimilarity.Term term); - public interface Factory { + public interface Factory extends ScriptFactory { SimilarityWeightScript newInstance(); } diff --git a/server/src/main/java/org/elasticsearch/script/StringSortScript.java b/server/src/main/java/org/elasticsearch/script/StringSortScript.java index 1c6c47dd21552..8c459fceed64d 100644 --- a/server/src/main/java/org/elasticsearch/script/StringSortScript.java +++ b/server/src/main/java/org/elasticsearch/script/StringSortScript.java @@ -45,7 +45,7 @@ public interface LeafFactory { /** * A factory to construct stateful {@link StringSortScript} factories for a specific index. */ - public interface Factory { + public interface Factory extends ScriptFactory { LeafFactory newFactory(Map params, SearchLookup lookup); } } diff --git a/server/src/main/java/org/elasticsearch/script/TemplateScript.java b/server/src/main/java/org/elasticsearch/script/TemplateScript.java index c053cf2b509d0..f7cf4590387d8 100644 --- a/server/src/main/java/org/elasticsearch/script/TemplateScript.java +++ b/server/src/main/java/org/elasticsearch/script/TemplateScript.java @@ -41,7 +41,7 @@ public Map getParams() { /** Run a template and return the resulting string, encoded in utf8 bytes. */ public abstract String execute(); - public interface Factory { + public interface Factory extends ScriptFactory { TemplateScript newInstance(Map params); } diff --git a/server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java b/server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java index b3d29ff50f278..7fcf56c5473cc 100644 --- a/server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java +++ b/server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java @@ -103,7 +103,7 @@ public interface LeafFactory { /** * A factory to construct stateful {@link TermsSetQueryScript} factories for a specific index. */ - public interface Factory { + public interface Factory extends ScriptFactory { LeafFactory newFactory(Map params, SearchLookup lookup); } } diff --git a/server/src/main/java/org/elasticsearch/script/UpdateScript.java b/server/src/main/java/org/elasticsearch/script/UpdateScript.java index ae0827ff83934..5dd08d5b602dd 100644 --- a/server/src/main/java/org/elasticsearch/script/UpdateScript.java +++ b/server/src/main/java/org/elasticsearch/script/UpdateScript.java @@ -58,7 +58,7 @@ public Map getCtx() { public abstract void execute(); - public interface Factory { + public interface Factory extends ScriptFactory { UpdateScript newInstance(Map params, Map ctx); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java index 50ef203880d94..e90df2ed33a0b 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java @@ -101,7 +101,7 @@ public SignificanceHeuristic rewrite(InternalAggregation.ReduceContext context) @Override public SignificanceHeuristic rewrite(QueryShardContext queryShardContext) { - SignificantTermsHeuristicScoreScript.Factory compiledScript = queryShardContext.getScriptService().compile(script, + SignificantTermsHeuristicScoreScript.Factory compiledScript = queryShardContext.compile(script, SignificantTermsHeuristicScoreScript.CONTEXT); return new ExecutableScriptHeuristic(script, compiledScript.newInstance()); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricAggregationBuilder.java index e650a968036cc..accee6fcb5250 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricAggregationBuilder.java @@ -209,14 +209,14 @@ protected ScriptedMetricAggregatorFactory doBuild(QueryShardContext queryShardCo ScriptedMetricAggContexts.InitScript.Factory compiledInitScript; Map initScriptParams; if (initScript != null) { - compiledInitScript = queryShardContext.getScriptService().compile(initScript, ScriptedMetricAggContexts.InitScript.CONTEXT); + compiledInitScript = queryShardContext.compile(initScript, ScriptedMetricAggContexts.InitScript.CONTEXT); initScriptParams = initScript.getParams(); } else { compiledInitScript = (p, a) -> null; initScriptParams = Collections.emptyMap(); } - ScriptedMetricAggContexts.MapScript.Factory compiledMapScript = queryShardContext.getScriptService().compile(mapScript, + ScriptedMetricAggContexts.MapScript.Factory compiledMapScript = queryShardContext.compile(mapScript, ScriptedMetricAggContexts.MapScript.CONTEXT); Map mapScriptParams = mapScript.getParams(); @@ -224,7 +224,7 @@ protected ScriptedMetricAggregatorFactory doBuild(QueryShardContext queryShardCo ScriptedMetricAggContexts.CombineScript.Factory compiledCombineScript; Map combineScriptParams; - compiledCombineScript = queryShardContext.getScriptService().compile(combineScript, + compiledCombineScript = queryShardContext.compile(combineScript, ScriptedMetricAggContexts.CombineScript.CONTEXT); combineScriptParams = combineScript.getParams(); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregationBuilder.java index f41027a291e26..a4b7fc2048c7b 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregationBuilder.java @@ -587,7 +587,7 @@ protected TopHitsAggregatorFactory doBuild(QueryShardContext queryShardContext, List fields = new ArrayList<>(); if (scriptFields != null) { for (ScriptField field : scriptFields) { - FieldScript.Factory factory = queryShardContext.getScriptService().compile(field.script(), FieldScript.CONTEXT); + FieldScript.Factory factory = queryShardContext.compile(field.script(), FieldScript.CONTEXT); FieldScript.LeafFactory searchScript = factory.newFactory(field.script().getParams(), queryShardContext.lookup()); fields.add(new org.elasticsearch.search.fetch.subphase.ScriptFieldsContext.ScriptField( field.fieldName(), searchScript, field.ignoreFailure())); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/MovingFunctionScript.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/MovingFunctionScript.java index 79e1f740729ce..8dae73138b272 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/MovingFunctionScript.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/MovingFunctionScript.java @@ -20,6 +20,7 @@ package org.elasticsearch.search.aggregations.pipeline; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import java.util.Map; @@ -35,7 +36,7 @@ public abstract class MovingFunctionScript { */ public abstract double execute(Map params, double[] values); - public interface Factory { + public interface Factory extends ScriptFactory { MovingFunctionScript newInstance(); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceConfig.java b/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceConfig.java index 4919b5bc9f9ad..ba599827b88af 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceConfig.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceConfig.java @@ -138,7 +138,7 @@ private static AggregationScript.LeafFactory createScript(Script script, QuerySh if (script == null) { return null; } else { - AggregationScript.Factory factory = context.getScriptService().compile(script, AggregationScript.CONTEXT); + AggregationScript.Factory factory = context.compile(script, AggregationScript.CONTEXT); return factory.newFactory(script.getParams(), context.lookup()); } } diff --git a/server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java b/server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java index d3fa3bb0a1fe9..6cf33830b0453 100644 --- a/server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java @@ -250,7 +250,7 @@ public SortFieldAndFormat build(QueryShardContext context) throws IOException { final IndexFieldData.XFieldComparatorSource fieldComparatorSource; switch (type) { case STRING: - final StringSortScript.Factory factory = context.getScriptService().compile(script, StringSortScript.CONTEXT); + final StringSortScript.Factory factory = context.compile(script, StringSortScript.CONTEXT); final StringSortScript.LeafFactory searchScript = factory.newFactory(script.getParams(), context.lookup()); fieldComparatorSource = new BytesRefFieldComparatorSource(null, null, valueMode, nested) { StringSortScript leafScript; @@ -279,7 +279,7 @@ protected void setScorer(Scorable scorer) { }; break; case NUMBER: - final NumberSortScript.Factory numberSortFactory = context.getScriptService().compile(script, NumberSortScript.CONTEXT); + final NumberSortScript.Factory numberSortFactory = context.compile(script, NumberSortScript.CONTEXT); final NumberSortScript.LeafFactory numberSortScript = numberSortFactory.newFactory(script.getParams(), context.lookup()); fieldComparatorSource = new DoubleValuesComparatorSource(null, Double.MAX_VALUE, valueMode, nested) { NumberSortScript leafScript; diff --git a/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestionBuilder.java b/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestionBuilder.java index 5b66badc733e0..7bbcc0d6c5405 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestionBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestionBuilder.java @@ -634,7 +634,7 @@ public SuggestionContext build(QueryShardContext context) throws IOException { } if (this.collateQuery != null) { - TemplateScript.Factory scriptFactory = context.getScriptService().compile(this.collateQuery, TemplateScript.CONTEXT); + TemplateScript.Factory scriptFactory = context.compile(this.collateQuery, TemplateScript.CONTEXT); suggestionContext.setCollateQueryScript(scriptFactory); if (this.collateParams != null) { suggestionContext.setCollateScriptParams(this.collateParams); diff --git a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java index 7b004db65da50..ed28a06500d5a 100644 --- a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; import org.elasticsearch.test.AbstractQueryTestCase; @@ -396,7 +397,7 @@ public boolean execute(Interval interval) { ScriptService scriptService = new ScriptService(Settings.EMPTY, Collections.emptyMap(), Collections.emptyMap()){ @Override @SuppressWarnings("unchecked") - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { assertEquals(IntervalFilterScript.CONTEXT, context); assertEquals(new Script("interval.start > 3"), script); return (FactoryType) factory; diff --git a/server/src/test/java/org/elasticsearch/script/ScriptContextTests.java b/server/src/test/java/org/elasticsearch/script/ScriptContextTests.java index 157b0969ae813..dc77fb0126262 100644 --- a/server/src/test/java/org/elasticsearch/script/ScriptContextTests.java +++ b/server/src/test/java/org/elasticsearch/script/ScriptContextTests.java @@ -23,28 +23,28 @@ public class ScriptContextTests extends ESTestCase { - public interface TwoNewInstance { + public interface TwoNewInstance extends ScriptFactory { String newInstance(int foo, int bar); String newInstance(int foo); - interface StatefulFactory { + interface StatefulFactory extends ScriptFactory { TwoNewInstance newFactory(); } } - public interface TwoNewFactory { + public interface TwoNewFactory extends ScriptFactory { String newFactory(int foo, int bar); String newFactory(int foo); } - public interface MissingNewInstance { + public interface MissingNewInstance extends ScriptFactory { String typoNewInstanceMethod(int foo); } public interface DummyScript { int execute(int foo); - interface Factory { + interface Factory extends ScriptFactory { DummyScript newInstance(); } } @@ -54,7 +54,7 @@ public interface DummyStatefulScript { interface StatefulFactory { DummyStatefulScript newInstance(); } - interface Factory { + interface Factory extends ScriptFactory { StatefulFactory newFactory(); } } diff --git a/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java b/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java index 38139103ed2ab..6e720b480934b 100644 --- a/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java +++ b/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java @@ -75,7 +75,7 @@ private ScriptService getMockScriptService(Settings settings) { } - public interface MiscContext { + public interface MiscContext extends ScriptFactory { void execute(); Object newInstance(); } diff --git a/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java b/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java index e6bbcf5dd57d0..2e61882256b77 100644 --- a/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java +++ b/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java @@ -34,6 +34,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; @@ -75,7 +76,12 @@ public String getType() { } @Override - public T compile(String scriptName, String scriptSource, ScriptContext context, Map params) { + public T compile( + String scriptName, + String scriptSource, + ScriptContext context, + Map params + ) { assert scriptSource.equals("explainable_script"); assert context == ScoreScript.CONTEXT; ScoreScript.Factory factory = (params1, lookup) -> new ScoreScript.LeafFactory() { diff --git a/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java b/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java index 9495bc444265e..5434f05c9aa00 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java @@ -34,6 +34,7 @@ import org.elasticsearch.plugins.ScriptPlugin; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.TemplateScript; import org.elasticsearch.search.suggest.phrase.DirectCandidateGeneratorBuilder; import org.elasticsearch.search.suggest.phrase.Laplace; @@ -1137,7 +1138,12 @@ public String getType() { } @Override - public T compile(String scriptName, String scriptSource, ScriptContext context, Map params) { + public T compile( + String scriptName, + String scriptSource, + ScriptContext context, + Map params + ) { if (context.instanceClazz != TemplateScript.class) { throw new UnsupportedOperationException(); } diff --git a/test/framework/src/main/java/org/elasticsearch/ingest/TestTemplateService.java b/test/framework/src/main/java/org/elasticsearch/ingest/TestTemplateService.java index 5bbf39d8fdc17..b5fcb2a37d7e3 100644 --- a/test/framework/src/main/java/org/elasticsearch/ingest/TestTemplateService.java +++ b/test/framework/src/main/java/org/elasticsearch/ingest/TestTemplateService.java @@ -23,6 +23,7 @@ import org.elasticsearch.script.MockScriptEngine; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.TemplateScript; @@ -48,7 +49,7 @@ private TestTemplateService(boolean compilationException) { } @Override - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { if (this.compilationException) { throw new RuntimeException("could not compile script"); } else { diff --git a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java index e278fbad85aff..84aad77377a30 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java @@ -82,7 +82,7 @@ public String getType() { } @Override - public T compile(String name, String source, ScriptContext context, Map params) { + public T compile(String name, String source, ScriptContext context, Map params) { // Scripts are always resolved using the script's source. For inline scripts, it's easy because they don't have names and the // source is always provided. For stored and file scripts, the source of the script must match the key of a predefined script. Function, Object> script = scripts.get(source); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/script/MockMustacheScriptEngine.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/script/MockMustacheScriptEngine.java index 4f9b125a9fd6b..16a95f0accdac 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/script/MockMustacheScriptEngine.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/script/MockMustacheScriptEngine.java @@ -39,7 +39,7 @@ public String getType() { } @Override - public T compile(String name, String script, ScriptContext context, Map params) { + public T compile(String name, String script, ScriptContext context, Map params) { if (script.contains("{{") && script.contains("}}")) { throw new IllegalArgumentException("Fix your test to not rely on mustache"); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/monitoring/test/MockPainlessScriptEngine.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/monitoring/test/MockPainlessScriptEngine.java index 2052cebe1d0e9..254542f355e5d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/monitoring/test/MockPainlessScriptEngine.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/monitoring/test/MockPainlessScriptEngine.java @@ -11,6 +11,7 @@ import org.elasticsearch.script.ScoreScript; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.script.ScriptFactory; import java.util.Collection; import java.util.Collections; @@ -42,7 +43,7 @@ public String getType() { } @Override - public T compile(String name, String script, ScriptContext context, Map options) { + public T compile(String name, String script, ScriptContext context, Map options) { if (context.instanceClazz.equals(ScoreScript.class)) { return context.factoryClazz.cast(new MockScoreScript(p -> 0.0)); } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/condition/WatcherConditionScript.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/condition/WatcherConditionScript.java index 1a5c8718bbd45..f5376aa424692 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/condition/WatcherConditionScript.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/condition/WatcherConditionScript.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.watcher.condition; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.xpack.core.watcher.execution.WatchExecutionContext; import org.elasticsearch.xpack.watcher.support.Variables; @@ -37,7 +38,7 @@ public Map getCtx() { return ctx; } - public interface Factory { + public interface Factory extends ScriptFactory { WatcherConditionScript newInstance(Map params, WatchExecutionContext watcherContext); } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transform/script/WatcherTransformScript.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transform/script/WatcherTransformScript.java index 57ee1e9f35c5d..3ef1f87cd608c 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transform/script/WatcherTransformScript.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transform/script/WatcherTransformScript.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.watcher.transform.script; import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.xpack.core.watcher.execution.WatchExecutionContext; import org.elasticsearch.xpack.core.watcher.watch.Payload; import org.elasticsearch.xpack.watcher.support.Variables; @@ -38,7 +39,7 @@ public Map getCtx() { return ctx; } - public interface Factory { + public interface Factory extends ScriptFactory { WatcherTransformScript newInstance(Map params, WatchExecutionContext watcherContext, Payload payload); } From 59d99b58aae32c509e11717b14c661cdfe52a727 Mon Sep 17 00:00:00 2001 From: Przemko Robakowski Date: Fri, 6 Dec 2019 21:57:06 +0100 Subject: [PATCH 104/686] Allow list of IPs in geoip ingest processor (#49573) * Allow list of IPs in geoip ingest processor This change lets you use array of IPs in addition to string in geoip processor source field. It will set array containing geoip data for each element in source, unless first_only parameter option is enabled, then only first found will be returned. Closes #46193 --- .../ingest/processors/geoip.asciidoc | 1 + .../ingest/geoip/GeoIpProcessor.java | 79 ++++++++---- .../ingest/geoip/GeoIpProcessorTests.java | 114 ++++++++++++++++-- .../test/ingest_geoip/20_geoip_processor.yml | 81 +++++++++++++ 4 files changed, 241 insertions(+), 34 deletions(-) diff --git a/docs/reference/ingest/processors/geoip.asciidoc b/docs/reference/ingest/processors/geoip.asciidoc index 58cc32d629760..84e8ed2c41a8a 100644 --- a/docs/reference/ingest/processors/geoip.asciidoc +++ b/docs/reference/ingest/processors/geoip.asciidoc @@ -25,6 +25,7 @@ uncompressed. The `ingest-geoip` config directory is located at `$ES_CONFIG/inge | `database_file` | no | GeoLite2-City.mmdb | The database filename in the geoip config directory. The ingest-geoip module ships with the GeoLite2-City.mmdb, GeoLite2-Country.mmdb and GeoLite2-ASN.mmdb files. | `properties` | no | [`continent_name`, `country_iso_code`, `region_iso_code`, `region_name`, `city_name`, `location`] * | Controls what properties are added to the `target_field` based on the geoip lookup. | `ignore_missing` | no | `false` | If `true` and `field` does not exist, the processor quietly exits without modifying the document +| `first_only` | no | `true` | If `true` only first found geoip data will be returned, even if `field` contains array |====== *Depends on what is available in `database_file`: diff --git a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpProcessor.java b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpProcessor.java index 5c82c68d93032..41300f71093a0 100644 --- a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpProcessor.java +++ b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpProcessor.java @@ -41,6 +41,7 @@ import java.net.InetAddress; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; @@ -68,6 +69,7 @@ public final class GeoIpProcessor extends AbstractProcessor { private final Set properties; private final boolean ignoreMissing; private final GeoIpCache cache; + private final boolean firstOnly; /** * Construct a geo-IP processor. @@ -79,15 +81,17 @@ public final class GeoIpProcessor extends AbstractProcessor { * @param properties the properties; ideally this is lazily-loaded once on first use * @param ignoreMissing true if documents with a missing value for the field should be ignored * @param cache a geo-IP cache + * @param firstOnly true if only first result should be returned in case of array */ GeoIpProcessor( - final String tag, - final String field, - final DatabaseReaderLazyLoader lazyLoader, - final String targetField, - final Set properties, - final boolean ignoreMissing, - final GeoIpCache cache) { + final String tag, + final String field, + final DatabaseReaderLazyLoader lazyLoader, + final String targetField, + final Set properties, + final boolean ignoreMissing, + final GeoIpCache cache, + boolean firstOnly) { super(tag); this.field = field; this.targetField = targetField; @@ -95,6 +99,7 @@ public final class GeoIpProcessor extends AbstractProcessor { this.properties = properties; this.ignoreMissing = ignoreMissing; this.cache = cache; + this.firstOnly = firstOnly; } boolean isIgnoreMissing() { @@ -103,7 +108,7 @@ boolean isIgnoreMissing() { @Override public IngestDocument execute(IngestDocument ingestDocument) throws IOException { - String ip = ingestDocument.getFieldValue(field, String.class, ignoreMissing); + Object ip = ingestDocument.getFieldValue(field, Object.class, ignoreMissing); if (ip == null && ignoreMissing) { return ingestDocument; @@ -111,11 +116,43 @@ public IngestDocument execute(IngestDocument ingestDocument) throws IOException throw new IllegalArgumentException("field [" + field + "] is null, cannot extract geoip information."); } - final InetAddress ipAddress = InetAddresses.forString(ip); + if (ip instanceof String) { + Map geoData = getGeoData((String) ip); + if (geoData.isEmpty() == false) { + ingestDocument.setFieldValue(targetField, geoData); + } + } else if (ip instanceof List) { + boolean match = false; + List> geoDataList = new ArrayList<>(((List) ip).size()); + for (Object ipAddr : (List) ip) { + if (ipAddr instanceof String == false) { + throw new IllegalArgumentException("array in field [" + field + "] should only contain strings"); + } + Map geoData = getGeoData((String) ipAddr); + if (geoData.isEmpty()) { + geoDataList.add(null); + continue; + } + if (firstOnly) { + ingestDocument.setFieldValue(targetField, geoData); + return ingestDocument; + } + match = true; + geoDataList.add(geoData); + } + if (match) { + ingestDocument.setFieldValue(targetField, geoDataList); + } + } else { + throw new IllegalArgumentException("field [" + field + "] should contain only string or array of strings"); + } + return ingestDocument; + } - Map geoData; + private Map getGeoData(String ip) throws IOException { String databaseType = lazyLoader.getDatabaseType(); - + final InetAddress ipAddress = InetAddresses.forString(ip); + Map geoData; if (databaseType.endsWith(CITY_DB_SUFFIX)) { try { geoData = retrieveCityGeoData(ipAddress); @@ -136,12 +173,9 @@ public IngestDocument execute(IngestDocument ingestDocument) throws IOException } } else { throw new ElasticsearchParseException("Unsupported database type [" + lazyLoader.getDatabaseType() - + "]", new IllegalStateException()); - } - if (geoData.isEmpty() == false) { - ingestDocument.setFieldValue(targetField, geoData); + + "]", new IllegalStateException()); } - return ingestDocument; + return geoData; } @Override @@ -360,14 +394,15 @@ public Factory(Map databaseReaders, GeoIpCache @Override public GeoIpProcessor create( - final Map registry, - final String processorTag, - final Map config) throws IOException { + final Map registry, + final String processorTag, + final Map config) throws IOException { String ipField = readStringProperty(TYPE, processorTag, config, "field"); String targetField = readStringProperty(TYPE, processorTag, config, "target_field", "geoip"); String databaseFile = readStringProperty(TYPE, processorTag, config, "database_file", "GeoLite2-City.mmdb"); List propertyNames = readOptionalList(TYPE, processorTag, config, "properties"); boolean ignoreMissing = readBooleanProperty(TYPE, processorTag, config, "ignore_missing", false); + boolean firstOnly = readBooleanProperty(TYPE, processorTag, config, "first_only", true); DatabaseReaderLazyLoader lazyLoader = databaseReaders.get(databaseFile); if (lazyLoader == null) { @@ -397,11 +432,11 @@ public GeoIpProcessor create( properties = DEFAULT_ASN_PROPERTIES; } else { throw newConfigurationException(TYPE, processorTag, "database_file", "Unsupported database type [" - + databaseType + "]"); + + databaseType + "]"); } } - return new GeoIpProcessor(processorTag, ipField, lazyLoader, targetField, properties, ignoreMissing, cache); + return new GeoIpProcessor(processorTag, ipField, lazyLoader, targetField, properties, ignoreMissing, cache, firstOnly); } } @@ -460,7 +495,7 @@ public static Property parseProperty(String databaseType, String value) { return property; } catch (IllegalArgumentException e) { throw new IllegalArgumentException("illegal property value [" + value + "]. valid values are " + - Arrays.toString(validProperties.toArray())); + Arrays.toString(validProperties.toArray())); } } } diff --git a/modules/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpProcessorTests.java b/modules/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpProcessorTests.java index b136fbae0376a..f0e9b30ae69ca 100644 --- a/modules/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpProcessorTests.java +++ b/modules/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpProcessorTests.java @@ -29,9 +29,11 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -39,13 +41,14 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; public class GeoIpProcessorTests extends ESTestCase { public void testCity() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); Map document = new HashMap<>(); document.put("source_field", "8.8.8.8"); @@ -70,7 +73,7 @@ public void testCity() throws Exception { public void testNullValueWithIgnoreMissing() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), true, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); IngestDocument originalIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.singletonMap("source_field", null)); IngestDocument ingestDocument = new IngestDocument(originalIngestDocument); @@ -81,7 +84,7 @@ public void testNullValueWithIgnoreMissing() throws Exception { public void testNonExistentWithIgnoreMissing() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), true, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); IngestDocument originalIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); IngestDocument ingestDocument = new IngestDocument(originalIngestDocument); processor.execute(ingestDocument); @@ -91,7 +94,7 @@ public void testNonExistentWithIgnoreMissing() throws Exception { public void testNullWithoutIgnoreMissing() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); IngestDocument originalIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.singletonMap("source_field", null)); IngestDocument ingestDocument = new IngestDocument(originalIngestDocument); @@ -102,7 +105,7 @@ public void testNullWithoutIgnoreMissing() throws Exception { public void testNonExistentWithoutIgnoreMissing() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); IngestDocument originalIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); IngestDocument ingestDocument = new IngestDocument(originalIngestDocument); Exception exception = expectThrows(Exception.class, () -> processor.execute(ingestDocument)); @@ -112,7 +115,7 @@ public void testNonExistentWithoutIgnoreMissing() throws Exception { public void testCity_withIpV6() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); String address = "2602:306:33d3:8000::3257:9652"; Map document = new HashMap<>(); @@ -141,7 +144,7 @@ public void testCity_withIpV6() throws Exception { public void testCityWithMissingLocation() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); Map document = new HashMap<>(); document.put("source_field", "80.231.5.0"); @@ -158,7 +161,7 @@ public void testCityWithMissingLocation() throws Exception { public void testCountry() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-Country.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); Map document = new HashMap<>(); document.put("source_field", "82.170.213.79"); @@ -178,7 +181,7 @@ public void testCountry() throws Exception { public void testCountryWithMissingLocation() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-Country.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); Map document = new HashMap<>(); document.put("source_field", "80.231.5.0"); @@ -196,7 +199,7 @@ public void testAsn() throws Exception { String ip = "82.171.64.0"; GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-ASN.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); Map document = new HashMap<>(); document.put("source_field", ip); @@ -215,7 +218,7 @@ public void testAsn() throws Exception { public void testAddressIsNotInTheDatabase() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); Map document = new HashMap<>(); document.put("source_field", "127.0.0.1"); @@ -228,7 +231,7 @@ public void testAddressIsNotInTheDatabase() throws Exception { public void testInvalid() throws Exception { GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, - new GeoIpCache(1000)); + new GeoIpCache(1000), false); Map document = new HashMap<>(); document.put("source_field", "www.google.com"); @@ -237,6 +240,93 @@ public void testInvalid() throws Exception { assertThat(e.getMessage(), containsString("not an IP string literal")); } + public void testListAllValid() throws Exception { + GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", + loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, + new GeoIpCache(1000), false); + + Map document = new HashMap<>(); + document.put("source_field", Arrays.asList("8.8.8.8", "82.171.64.0")); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); + processor.execute(ingestDocument); + + @SuppressWarnings("unchecked") + List> geoData = (List>) ingestDocument.getSourceAndMetadata().get("target_field"); + + Map location = new HashMap<>(); + location.put("lat", 37.751d); + location.put("lon", -97.822d); + assertThat(geoData.get(0).get("location"), equalTo(location)); + + assertThat(geoData.get(1).get("city_name"), equalTo("Hoensbroek")); + } + + public void testListPartiallyValid() throws Exception { + GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", + loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, + new GeoIpCache(1000), false); + + Map document = new HashMap<>(); + document.put("source_field", Arrays.asList("8.8.8.8", "127.0.0.1")); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); + processor.execute(ingestDocument); + + @SuppressWarnings("unchecked") + List> geoData = (List>) ingestDocument.getSourceAndMetadata().get("target_field"); + + Map location = new HashMap<>(); + location.put("lat", 37.751d); + location.put("lon", -97.822d); + assertThat(geoData.get(0).get("location"), equalTo(location)); + + assertThat(geoData.get(1), nullValue()); + } + + public void testListNoMatches() throws Exception { + GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", + loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, + new GeoIpCache(1000), false); + + Map document = new HashMap<>(); + document.put("source_field", Arrays.asList("127.0.0.1", "127.0.0.1")); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); + processor.execute(ingestDocument); + + assertFalse(ingestDocument.hasField("target_field")); + } + + public void testListFirstOnly() throws Exception { + GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", + loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, + new GeoIpCache(1000), true); + + Map document = new HashMap<>(); + document.put("source_field", Arrays.asList("8.8.8.8", "127.0.0.1")); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); + processor.execute(ingestDocument); + + @SuppressWarnings("unchecked") + Map geoData = (Map) ingestDocument.getSourceAndMetadata().get("target_field"); + + Map location = new HashMap<>(); + location.put("lat", 37.751d); + location.put("lon", -97.822d); + assertThat(geoData.get("location"), equalTo(location)); + } + + public void testListFirstOnlyNoMatches() throws Exception { + GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field", + loader("/GeoLite2-City.mmdb"), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false, + new GeoIpCache(1000), true); + + Map document = new HashMap<>(); + document.put("source_field", Arrays.asList("127.0.0.1", "127.0.0.2")); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); + processor.execute(ingestDocument); + + assertThat(ingestDocument.getSourceAndMetadata().containsKey("target_field"), is(false)); + } + private DatabaseReaderLazyLoader loader(final String path) { final Supplier databaseInputStreamSupplier = () -> GeoIpProcessor.class.getResourceAsStream(path); final CheckedSupplier loader = diff --git a/modules/ingest-geoip/src/test/resources/rest-api-spec/test/ingest_geoip/20_geoip_processor.yml b/modules/ingest-geoip/src/test/resources/rest-api-spec/test/ingest_geoip/20_geoip_processor.yml index 27ab1f4e8747d..f6bdce0532ace 100644 --- a/modules/ingest-geoip/src/test/resources/rest-api-spec/test/ingest_geoip/20_geoip_processor.yml +++ b/modules/ingest-geoip/src/test/resources/rest-api-spec/test/ingest_geoip/20_geoip_processor.yml @@ -37,6 +37,87 @@ - match: { _source.geoip.region_name: "Minnesota" } - match: { _source.geoip.continent_name: "North America" } +--- +"Test geoip processor with list": + - do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "geoip" : { + "field" : "field1", + "first_only" : false + } + } + ] + } + - match: { acknowledged: true } + + - do: + index: + index: test + id: 1 + pipeline: "my_pipeline" + body: {field1: ["128.101.101.101", "127.0.0.1"]} + + - do: + get: + index: test + id: 1 + - match: { _source.field1: ["128.101.101.101", "127.0.0.1"] } + - length: { _source.geoip: 2 } + - length: { _source.geoip.0: 6 } + - match: { _source.geoip.0.city_name: "Minneapolis" } + - match: { _source.geoip.0.country_iso_code: "US" } + - match: { _source.geoip.0.location.lon: -93.2548 } + - match: { _source.geoip.0.location.lat: 44.9399 } + - match: { _source.geoip.0.region_iso_code: "US-MN" } + - match: { _source.geoip.0.region_name: "Minnesota" } + - match: { _source.geoip.0.continent_name: "North America" } + - match: { _source.geoip.1: null } + +--- +"Test geoip processor with lists, first only": + - do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "geoip" : { + "field" : "field1" + } + } + ] + } + - match: { acknowledged: true } + + - do: + index: + index: test + id: 1 + pipeline: "my_pipeline" + body: {field1: ["127.0.0.1", "128.101.101.101", "128.101.101.101"]} + + - do: + get: + index: test + id: 1 + - match: { _source.field1: ["127.0.0.1", "128.101.101.101", "128.101.101.101"] } + - length: { _source.geoip: 6 } + - match: { _source.geoip.city_name: "Minneapolis" } + - match: { _source.geoip.country_iso_code: "US" } + - match: { _source.geoip.location.lon: -93.2548 } + - match: { _source.geoip.location.lat: 44.9399 } + - match: { _source.geoip.region_iso_code: "US-MN" } + - match: { _source.geoip.region_name: "Minnesota" } + - match: { _source.geoip.continent_name: "North America" } + --- "Test geoip processor with fields": - do: From 29c8a6d21d88dae367cb42d5de4ffa29fe0a0267 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Fri, 6 Dec 2019 14:44:20 -0800 Subject: [PATCH 105/686] Upgrade to Gradle 6.0 (#49211) This upgrade required a few significant changes. Firstly, the build scan plugin has been renamed, and changed to be a Settings plugin rather than a project plugin so the declaration of this has moved to our settings.gradle file. Second, we were using a rather old version of the Nebula ospackage plugin for building deb and rpm packages, the migration to the latest version required some updates to get things working as expected as we had some workarounds in place that are no longer applicable with the latest bug fixes. --- .ci/java-versions.properties | 3 +- build.gradle | 1 - buildSrc/build.gradle | 2 +- .../src/main/resources/minimumGradleVersion | 2 +- .../src/testKit/thirdPartyAudit/build.gradle | 3 ++ distribution/packages/build.gradle | 22 ++++++------- gradle/build-complete.gradle | 2 ++ gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 58702 bytes gradle/wrapper/gradle-wrapper.properties | 4 +-- gradlew | 29 ++++++++---------- settings.gradle | 4 +++ 11 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.ci/java-versions.properties b/.ci/java-versions.properties index a63e17527daee..1c1e813553670 100644 --- a/.ci/java-versions.properties +++ b/.ci/java-versions.properties @@ -8,4 +8,5 @@ ES_BUILD_JAVA=openjdk12 ES_RUNTIME_JAVA=openjdk11 GRADLE_TASK=build - +# Workaround for https://github.com/gradle/gradle/issues/11426 +OPENSHIFT_IP=0.0.0.0 diff --git a/build.gradle b/build.gradle index d38c4aa3da790..df1641dcc9efe 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,6 @@ import org.gradle.util.GradleVersion import static org.elasticsearch.gradle.tool.Boilerplate.maybeConfigure plugins { - id 'com.gradle.build-scan' version '2.4.2' id 'lifecycle-base' id 'elasticsearch.global-build-info' id "com.diffplug.gradle.spotless" version "3.24.2" apply false diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index de883187547fd..2d28c79e80248 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -29,7 +29,7 @@ group = 'org.elasticsearch.gradle' String minimumGradleVersion = file('src/main/resources/minimumGradleVersion').text.trim() if (GradleVersion.current() < GradleVersion.version(minimumGradleVersion)) { - throw new GradleException("Gradle ${minimumGradleVersion}+ is required to build elasticsearch") + throw new GradleException("Gradle ${minimumGradleVersion}+ is required to build elasticsearch") } if (project == rootProject) { diff --git a/buildSrc/src/main/resources/minimumGradleVersion b/buildSrc/src/main/resources/minimumGradleVersion index 2a06a418a7736..6d54bbd77512d 100644 --- a/buildSrc/src/main/resources/minimumGradleVersion +++ b/buildSrc/src/main/resources/minimumGradleVersion @@ -1 +1 @@ -5.6.4 \ No newline at end of file +6.0.1 \ No newline at end of file diff --git a/buildSrc/src/testKit/thirdPartyAudit/build.gradle b/buildSrc/src/testKit/thirdPartyAudit/build.gradle index 253d3f0acc1c9..629460c2627c1 100644 --- a/buildSrc/src/testKit/thirdPartyAudit/build.gradle +++ b/buildSrc/src/testKit/thirdPartyAudit/build.gradle @@ -16,6 +16,9 @@ repositories { maven { name = "local-test" url = file("sample_jars/build/testrepo") + metadataSources { + artifact() + } } jcenter() } diff --git a/distribution/packages/build.gradle b/distribution/packages/build.gradle index 695654bca34b6..99de6773c0f92 100644 --- a/distribution/packages/build.gradle +++ b/distribution/packages/build.gradle @@ -51,16 +51,8 @@ import java.util.regex.Pattern * dpkg -c path/to/elasticsearch.deb */ -buildscript { - repositories { - maven { - name "gradle-plugins" - url "https://plugins.gradle.org/m2/" - } - } - dependencies { - classpath 'com.netflix.nebula:gradle-ospackage-plugin:4.7.1' - } +plugins { + id "nebula.ospackage-base" version "8.0.3" } void addProcessFilesTask(String type, boolean oss, boolean jdk) { @@ -115,10 +107,12 @@ Closure commonPackageConfig(String type, boolean oss, boolean jdk) { arch(type == 'deb' ? 'amd64' : 'X86_64') // Follow elasticsearch's file naming convention String jdkString = jdk ? "" : "no-jdk-" - archiveName "${packageName}-${project.version}-${jdkString}${archString}.${type}" - String prefix = "${oss ? 'oss-' : ''}${jdk ? '' : 'no-jdk-'}${type}" destinationDir = file("${prefix}/build/distributions") + + // SystemPackagingTask overrides default archive task convention mappings, but doesn't provide a setter so we have to override the convention mapping itself + conventionMapping.archiveFile = { objects.fileProperty().fileValue(file("${destinationDir}/${packageName}-${project.version}-${jdkString}${archString}.${type}")) } + String packagingFiles = "build/packaging/${oss ? 'oss-' : ''}${jdk ? '' : 'no-jdk-'}${type}" String scripts = "${packagingFiles}/scripts" @@ -157,7 +151,9 @@ Closure commonPackageConfig(String type, boolean oss, boolean jdk) { eachFile { FileCopyDetails fcp -> String[] segments = fcp.relativePath.segments for (int i = segments.length - 2; i > 2; --i) { - directory('/' + segments[0..i].join('/'), 0755) + if (type == 'rpm') { + directory('/' + segments[0..i].join('/'), 0755) + } if (segments[-2] == 'bin' || segments[-1] == 'jspawnhelper') { fcp.mode = 0755 } else { diff --git a/gradle/build-complete.gradle b/gradle/build-complete.gradle index 3ce58d19f537a..32c7bc4b068d1 100644 --- a/gradle/build-complete.gradle +++ b/gradle/build-complete.gradle @@ -38,6 +38,8 @@ if (buildNumber) { fileset(dir: "${gradle.gradleUserHomeDir}/daemon/${gradle.gradleVersion}", followsymlinks: false) { include(name: "**/daemon-${ProcessHandle.current().pid()}*.log") } + + fileset(dir: "${gradle.gradleUserHomeDir}/workers", followsymlinks: false) } } catch (Exception e) { logger.lifecycle("Failed to archive additional logs", e) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..cc4fdc293d0e50b0ad9b65c16e7ddd1db2f6025b 100644 GIT binary patch delta 16535 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 Date: Fri, 6 Dec 2019 15:32:07 -0800 Subject: [PATCH 106/686] [DOCS] Move anomaly detection job resource definitions into APIs (#49700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: István Zoltán Szabó --- .../client/ml/job/config/Detector.java | 6 +- docs/build.gradle | 1 + .../anomaly-detection/apis/close-job.asciidoc | 18 +- .../apis/datafeedresource.asciidoc | 2 +- .../apis/delete-job.asciidoc | 3 +- .../anomaly-detection/apis/flush-job.asciidoc | 3 +- .../anomaly-detection/apis/forecast.asciidoc | 5 +- .../apis/get-bucket.asciidoc | 3 +- .../apis/get-category.asciidoc | 3 +- .../apis/get-influencer.asciidoc | 3 +- .../apis/get-job-stats.asciidoc | 23 +- .../anomaly-detection/apis/get-job.asciidoc | 173 +++-- .../apis/get-overall-buckets.asciidoc | 9 +- .../apis/get-record.asciidoc | 3 +- .../apis/get-snapshot.asciidoc | 3 +- .../apis/jobresource.asciidoc | 561 ---------------- .../anomaly-detection/apis/open-job.asciidoc | 3 +- .../anomaly-detection/apis/post-data.asciidoc | 3 +- .../apis/put-calendar-job.asciidoc | 4 +- .../apis/put-datafeed.asciidoc | 7 +- .../apis/put-filter.asciidoc | 2 +- .../anomaly-detection/apis/put-job.asciidoc | 66 +- .../apis/revert-snapshot.asciidoc | 3 +- .../apis/update-datafeed.asciidoc | 7 +- .../apis/update-job.asciidoc | 114 ++-- .../apis/update-snapshot.asciidoc | 3 +- .../apis/validate-detector.asciidoc | 3 +- .../apis/validate-job.asciidoc | 2 +- .../ml/anomaly-detection/categories.asciidoc | 52 +- .../ml/anomaly-detection/functions.asciidoc | 3 +- .../functions/count.asciidoc | 12 +- .../anomaly-detection/functions/geo.asciidoc | 4 +- .../anomaly-detection/functions/info.asciidoc | 4 +- .../functions/metric.asciidoc | 22 +- .../anomaly-detection/functions/rare.asciidoc | 8 +- .../anomaly-detection/functions/sum.asciidoc | 8 +- .../anomaly-detection/functions/time.asciidoc | 8 +- .../apis/delete-dfanalytics.asciidoc | 3 +- .../apis/explain-dfanalytics.asciidoc | 6 +- .../apis/get-dfanalytics-stats.asciidoc | 19 +- .../apis/put-dfanalytics.asciidoc | 7 +- .../apis/start-dfanalytics.asciidoc | 5 +- .../apis/stop-dfanalytics.asciidoc | 5 +- docs/reference/ml/ml-shared.asciidoc | 621 +++++++++++++++++- docs/reference/redirects.asciidoc | 9 + docs/reference/rest-api/defs.asciidoc | 2 - docs/reference/settings/ml-settings.asciidoc | 2 +- 47 files changed, 1007 insertions(+), 829 deletions(-) delete mode 100644 docs/reference/ml/anomaly-detection/apis/jobresource.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Detector.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Detector.java index 44fc18032d29b..48a04d1225126 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Detector.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Detector.java @@ -34,9 +34,9 @@ /** * Defines the fields and functions used in the analysis. A combination of field_name, * by_field_name and over_field_name can be used depending on the specific - * function chosen. For more information see - * configuring - * detectors and detector functions. + * function chosen. For more information see the + * create anomaly detection + * jobs API and detector functions. */ public class Detector implements ToXContentObject { diff --git a/docs/build.gradle b/docs/build.gradle index f7c0fc7e080b1..a39c7e0d9f3bd 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -29,6 +29,7 @@ buildRestTests.expectedUnconvertedCandidates = [ 'reference/ml/anomaly-detection/apis/get-category.asciidoc', 'reference/ml/anomaly-detection/apis/get-influencer.asciidoc', 'reference/ml/anomaly-detection/apis/get-job-stats.asciidoc', + 'reference/ml/anomaly-detection/apis/get-job.asciidoc', 'reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc', 'reference/ml/anomaly-detection/apis/get-record.asciidoc', 'reference/ml/anomaly-detection/apis/get-snapshot.asciidoc', diff --git a/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc index bdbbc88a50173..7f9fd0489ab77 100644 --- a/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc @@ -60,25 +60,15 @@ results the job might have recently produced or might produce in the future. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {anomaly-job}. It can be a job - identifier, a group name, or a wildcard expression. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-wildcard] [[ml-close-job-query-parms]] ==== {api-query-parms-title} `allow_no_jobs`:: - (Optional, boolean) Specifies what to do when the request: -+ --- -* Contains wildcard expressions and there are no jobs that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns an empty `jobs` array -when there are no matches and the subset of results when there are partial -matches. If this parameter is `false`, the request returns a `404` status code -when there are no matches or only partial matches. --- +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-jobs] `force`:: (Optional, boolean) Use to close a failed job, or to forcefully close a job diff --git a/docs/reference/ml/anomaly-detection/apis/datafeedresource.asciidoc b/docs/reference/ml/anomaly-detection/apis/datafeedresource.asciidoc index 389c9d704eacd..864e71e35bdbe 100644 --- a/docs/reference/ml/anomaly-detection/apis/datafeedresource.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/datafeedresource.asciidoc @@ -50,7 +50,7 @@ A {dfeed} resource has the following properties: `script_fields`:: (object) Specifies scripts that evaluate custom expressions and returns script fields to the {dfeed}. - The <> in a job can contain + The detector configuration objects in a job can contain functions that use these script fields. For more information, see {stack-ov}/ml-configuring-transform.html[Transforming Data With Script Fields]. diff --git a/docs/reference/ml/anomaly-detection/apis/delete-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-job.asciidoc index 096939184ea25..0c04ec468bbbf 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-job.asciidoc @@ -39,7 +39,8 @@ separated list. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {anomaly-job}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-delete-job-query-parms]] ==== {api-query-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/flush-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/flush-job.asciidoc index 7afef6eabde4c..f6e81a3b26131 100644 --- a/docs/reference/ml/anomaly-detection/apis/flush-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/flush-job.asciidoc @@ -37,7 +37,8 @@ opened again before analyzing further data. ==== {api-path-parms-title} ``:: -(string) Required. Identifier for the job. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-flush-job-query-parms]] ==== {api-query-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc b/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc index 3bfc8b51b4532..0ec17ea9bd25e 100644 --- a/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc @@ -29,7 +29,7 @@ See {stack-ov}/ml-overview.html#ml-forecasting[Forecasting the future]. =============================== * If you use an `over_field_name` property in your job, you cannot create a -forecast. For more information about this property, see <>. +forecast. For more information about this property, see <>. * The job must be open when you create a forecast. Otherwise, an error occurs. =============================== @@ -37,7 +37,8 @@ forecast. For more information about this property, see <>. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the job. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-forecast-request-body]] ==== {api-request-body-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc index 91c473ebec915..027de1385e83f 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc @@ -36,7 +36,8 @@ bucket. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {anomaly-job}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: (Optional, string) The timestamp of a single bucket result. If you do not diff --git a/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc index 1f7955873451c..3280b79534f50 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc @@ -35,7 +35,8 @@ For more information about categories, see ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {anomaly-job}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: (Optional, long) Identifier for the category. If you do not specify this diff --git a/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc index a2da47720c9ea..2165d8ef9f7f9 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc @@ -27,7 +27,8 @@ privileges. See <> and ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {anomaly-job}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-get-influencer-request-body]] ==== {api-request-body-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc index 9c7bcc6e7b398..99928e43ca5f3 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc @@ -40,26 +40,15 @@ IMPORTANT: This API returns a maximum of 10,000 jobs. ==== {api-path-parms-title} ``:: - (Optional, string) An identifier for the {anomaly-job}. It can be a - job identifier, a group name, or a wildcard expression. If you do not specify - one of these options, the API returns statistics for all {anomaly-jobs}. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-default] [[ml-get-job-stats-query-parms]] ==== {api-query-parms-title} `allow_no_jobs`:: - (Optional, boolean) Specifies what to do when the request: -+ --- -* Contains wildcard expressions and there are no jobs that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns an empty `jobs` array -when there are no matches and the subset of results when there are partial -matches. If this parameter is `false`, the request returns a `404` status code -when there are no matches or only partial matches. --- +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-jobs] [[ml-get-job-stats-results]] ==== {api-response-body-title} @@ -67,8 +56,8 @@ when there are no matches or only partial matches. The API returns the following information: `jobs`:: - (array) An array of {anomaly-job} statistics objects. - For more information, see <>. +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=jobs-stats-anomaly-detection] [[ml-get-job-stats-response-codes]] ==== {api-response-codes-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc index a816bcd3e1ddc..7aeafde9ee884 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc @@ -40,35 +40,101 @@ IMPORTANT: This API returns a maximum of 10,000 jobs. ==== {api-path-parms-title} ``:: - (Optional, string) Identifier for the {anomaly-job}. It can be a job - identifier, a group name, or a wildcard expression. If you do not specify one - of these options, the API returns information for all {anomaly-jobs}. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-default] [[ml-get-job-query-parms]] ==== {api-query-parms-title} `allow_no_jobs`:: - (Optional, boolean) Specifies what to do when the request: +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-jobs] + + +[[ml-get-job-results]] +==== {api-response-body-title} + +The API returns an array of {anomaly-job} resources, which have the following +properties: + +`allow_lazy_open`:: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-lazy-open] + +[[get-analysisconfig]]`analysis_config`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=analysis-config] + +[[get-analysislimits]]`analysis_limits`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=analysis-limits] + +`background_persist_interval`:: +(time units) +include::{docdir}/ml/ml-shared.asciidoc[tag=background-persist-interval] + +`create_time`:: +(string) The time the job was created. For example, `1491007356077`. This +property is informational; you cannot change its value. + +[[get-customsettings]]`custom_settings`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-settings] + +[[get-datadescription]]`data_description`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=data-description] + +`description`:: +(string) An optional description of the job. + +`finished_time`:: +(string) If the job closed or failed, this is the time the job finished, +otherwise it is `null`. This property is informational; you cannot change its +value. + +`groups`:: +(array of strings) +include::{docdir}/ml/ml-shared.asciidoc[tag=groups] + +`job_id`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-define] + -- -* Contains wildcard expressions and there are no jobs that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns an empty `jobs` array -when there are no matches and the subset of results when there are partial -matches. If this parameter is `false`, the request returns a `404` status code -when there are no matches or only partial matches. +This property is informational; you cannot change the identifier for existing +jobs. -- -[[ml-get-job-results]] -==== {api-response-body-title} +`job_type`:: +(string) Reserved for future use, currently set to `anomaly_detector`. -The API returns the following information: +`job_version`:: +(string) The version of {es} that existed on the node when the job was created. -`jobs`:: - (array) An array of {anomaly-job} resources. - For more information, see <>. +[[get-modelplotconfig]]`model_plot_config`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-plot-config] + +`model_snapshot_id`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-snapshot-id] ++ +-- +This property is informational; you cannot change its value. +-- + +`model_snapshot_retention_days`:: +(long) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-snapshot-retention-days] + +`renormalization_window_days`:: +(long) +include::{docdir}/ml/ml-shared.asciidoc[tag=renormalization-window-days] + +`results_index_name`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=results-index-name] [[ml-get-job-response-codes]] ==== {api-response-codes-title} @@ -80,53 +146,68 @@ The API returns the following information: [[ml-get-job-example]] ==== {api-examples-title} -The following example gets configuration information for the `total-requests` job: +//The following example gets configuration information for the `total-requests` job: [source,console] -------------------------------------------------- -GET _ml/anomaly_detectors/total-requests +GET _ml/anomaly_detectors/high_sum_total_sales -------------------------------------------------- -// TEST[skip:setup:server_metrics_job] +// TEST[skip:Kibana sample data] The API returns the following results: -[source,console-result] +[source,js] ---- { "count": 1, "jobs": [ { - "job_id": "total-requests", - "job_type": "anomaly_detector", - "job_version": "7.0.0-alpha1", - "description": "Total sum of requests", - "create_time": 1517011406091, - "analysis_config": { - "bucket_span": "10m", - "detectors": [ + "job_id" : "high_sum_total_sales", + "job_type" : "anomaly_detector", + "job_version" : "8.0.0", + "groups" : [ + "kibana_sample_data", + "kibana_sample_ecommerce" + ], + "description" : "Find customers spending an unusually high amount in an hour", + "create_time" : 1575402224732, + "finished_time" : 1575402238311, + "analysis_config" : { + "bucket_span" : "1h", + "detectors" : [ { - "detector_description": "Sum of total", - "function": "sum", - "field_name": "total", - "detector_index": 0 + "detector_description" : "High total sales", + "function" : "high_sum", + "field_name" : "taxful_total_price", + "over_field_name" : "customer_full_name.keyword", + "detector_index" : 0 } ], - "influencers": [ ] + "influencers" : [ + "customer_full_name.keyword", + "category.keyword" + ] + }, + "analysis_limits" : { + "model_memory_limit" : "10mb", + "categorization_examples_limit" : 4 + }, + "data_description" : { + "time_field" : "order_date", + "time_format" : "epoch_ms" }, - "analysis_limits": { - "model_memory_limit": "1024mb", - "categorization_examples_limit": 4 + "model_plot_config" : { + "enabled" : true }, - "data_description": { - "time_field": "timestamp", - "time_format": "epoch_ms" + "model_snapshot_retention_days" : 1, + "custom_settings" : { + "created_by" : "ml-module-sample", + ... }, - "model_snapshot_retention_days": 1, - "results_index_name": "shared", - "allow_lazy_open": false + "model_snapshot_id" : "1575402237", + "results_index_name" : "shared", + "allow_lazy_open" : false } ] } ---- -// TESTRESPONSE[s/"7.0.0-alpha1"/$body.$_path/] -// TESTRESPONSE[s/1517011406091/$body.$_path/] diff --git a/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc index 43a7de51d9899..62acd7902b9ff 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc @@ -54,17 +54,14 @@ a span equal to the jobs' largest bucket span. [[ml-get-overall-buckets-path-parms]] ==== {api-path-parms-title} -``:: - (Required, string) Identifier for the {anomaly-job}. It can be a job - identifier, a group name, a comma-separated list of jobs or groups, or a - wildcard expression. +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-wildcard-list] [[ml-get-overall-buckets-request-body]] ==== {api-request-body-title} `allow_no_jobs`:: - (Optional, boolean) If `false` and the `job_id` does not match any - {anomaly-jobs}, an error occurs. The default value is `true`. +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-jobs] `bucket_span`:: (Optional, string) The span of the overall buckets. Must be greater or equal diff --git a/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc index a850524872c0a..b5bbb15580e19 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc @@ -26,7 +26,8 @@ privileges. See <> and <>. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {anomaly-job}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-get-record-request-body]] ==== {api-request-body-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc index 04d09b50d3313..94b67f6f98b9d 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc @@ -26,7 +26,8 @@ Retrieves information about model snapshots. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {anomaly-job}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: (Optional, string) Identifier for the model snapshot. If you do not specify diff --git a/docs/reference/ml/anomaly-detection/apis/jobresource.asciidoc b/docs/reference/ml/anomaly-detection/apis/jobresource.asciidoc deleted file mode 100644 index 3699e73ca51c3..0000000000000 --- a/docs/reference/ml/anomaly-detection/apis/jobresource.asciidoc +++ /dev/null @@ -1,561 +0,0 @@ -[role="xpack"] -[testenv="platinum"] -[[ml-job-resource]] -=== Job resources - -A job resource has the following properties: - -`analysis_config`:: - (object) The analysis configuration, which specifies how to analyze the data. - See <>. - -`analysis_limits`:: - (object) Defines approximate limits on the memory resource requirements for the job. - See <>. - -`background_persist_interval`:: - (time units) Advanced configuration option. - The time between each periodic persistence of the model. - The default value is a randomized value between 3 to 4 hours, which avoids - all jobs persisting at exactly the same time. The smallest allowed value is - 1 hour. -+ --- -TIP: For very large models (several GB), persistence could take 10-20 minutes, -so do not set the `background_persist_interval` value too low. - --- - -`create_time`:: - (string) The time the job was created. For example, `1491007356077`. This - property is informational; you cannot change its value. - -`custom_settings`:: - (object) Advanced configuration option. Contains custom meta data about the - job. For example, it can contain custom URL information as shown in - {stack-ov}/ml-configuring-url.html[Adding Custom URLs to Machine Learning Results]. - -`data_description`:: - (object) Describes the data format and how APIs parse timestamp fields. - See <>. - -`description`:: - (string) An optional description of the job. - -`finished_time`:: - (string) If the job closed or failed, this is the time the job finished, - otherwise it is `null`. This property is informational; you cannot change its - value. - -`groups`:: - (array of strings) A list of job groups. A job can belong to no groups or - many. For example, `["group1", "group2"]`. - -`job_id`:: - (string) The unique identifier for the job. This identifier can contain - lowercase alphanumeric characters (a-z and 0-9), hyphens, and underscores. It - must start and end with alphanumeric characters. This property is - informational; you cannot change the identifier for existing jobs. - -`job_type`:: - (string) Reserved for future use, currently set to `anomaly_detector`. - -`job_version`:: - (string) The version of {es} that existed on the node when the job was created. - -`model_plot_config`:: - (object) Configuration properties for storing additional model information. - See <>. - -`model_snapshot_id`:: - (string) A numerical character string that uniquely identifies the model - snapshot. For example, `1491007364`. This property is informational; you - cannot change its value. For more information about model snapshots, see - <>. - -`model_snapshot_retention_days`:: - (long) The time in days that model snapshots are retained for the job. - Older snapshots are deleted. The default value is `1`, which means snapshots - are retained for one day (twenty-four hours). - -`renormalization_window_days`:: - (long) Advanced configuration option. - The period over which adjustments to the score are applied, as new data is seen. - The default value is the longer of 30 days or 100 `bucket_spans`. - -`results_index_name`:: - (string) The name of the index in which to store the {ml} results. - The default value is `shared`, - which corresponds to the index name `.ml-anomalies-shared` - -`results_retention_days`:: - (long) Advanced configuration option. - The number of days for which job results are retained. - Once per day at 00:30 (server time), results older than this period are - deleted from Elasticsearch. The default value is null, which means results - are retained. - -`allow_lazy_open`:: - (boolean) Advanced configuration option. - Whether this job should be allowed to open when there is insufficient - {ml} node capacity for it to be immediately assigned to a node. - The default is `false`, which means that the <> - will return an error if a {ml} node with capacity to run the - job cannot immediately be found. (However, this is also subject to - the cluster-wide `xpack.ml.max_lazy_ml_nodes` setting - see - <>.) If this option is set to `true` then - the <> will not return an error, and the job will - wait in the `opening` state until sufficient {ml} node capacity - is available. - -[[ml-analysisconfig]] -==== Analysis Configuration Objects - -An analysis configuration object has the following properties: - -`bucket_span`:: - (time units) The size of the interval that the analysis is aggregated into, - typically between `5m` and `1h`. The default value is `5m`. For more - information about time units, see <>. - -`categorization_field_name`:: - (string) If this property is specified, the values of the specified field will - be categorized. The resulting categories must be used in a detector by setting - `by_field_name`, `over_field_name`, or `partition_field_name` to the keyword - `mlcategory`. For more information, see - {stack-ov}/ml-configuring-categories.html[Categorizing Log Messages]. - -`categorization_filters`:: - (array of strings) If `categorization_field_name` is specified, - you can also define optional filters. This property expects an array of - regular expressions. The expressions are used to filter out matching sequences - from the categorization field values. You can use this functionality to fine - tune the categorization by excluding sequences from consideration when - categories are defined. For example, you can exclude SQL statements that - appear in your log files. For more information, see - {stack-ov}/ml-configuring-categories.html[Categorizing Log Messages]. - This property cannot be used at the same time as `categorization_analyzer`. - If you only want to define simple regular expression filters that are applied - prior to tokenization, setting this property is the easiest method. - If you also want to customize the tokenizer or post-tokenization filtering, - use the `categorization_analyzer` property instead and include the filters as - `pattern_replace` character filters. The effect is exactly the same. - -`categorization_analyzer`:: - (object or string) If `categorization_field_name` is specified, you can also - define the analyzer that is used to interpret the categorization field. This - property cannot be used at the same time as `categorization_filters`. See - <>. - -`detectors`:: - (array) An array of detector configuration objects, - which describe the anomaly detectors that are used in the job. - See <>. + -+ --- -NOTE: If the `detectors` array does not contain at least one detector, -no analysis can occur and an error is returned. - --- - -`influencers`:: - (array of strings) A comma separated list of influencer field names. - Typically these can be the by, over, or partition fields that are used in the - detector configuration. You might also want to use a field name that is not - specifically named in a detector, but is available as part of the input data. - When you use multiple detectors, the use of influencers is recommended as it - aggregates results for each influencer entity. - -`latency`:: - (time units) The size of the window in which to expect data that is out of - time order. The default value is 0 (no latency). If you specify a non-zero - value, it must be greater than or equal to one second. For more information - about time units, see <>. -+ --- -NOTE: Latency is only applicable when you send data by using -the <> API. - --- - -`multivariate_by_fields`:: - (boolean) This functionality is reserved for internal use. It is not supported - for use in customer environments and is not subject to the support SLA of - official GA features. -+ --- -If set to `true`, the analysis will automatically find correlations -between metrics for a given `by` field value and report anomalies when those -correlations cease to hold. For example, suppose CPU and memory usage on host A -is usually highly correlated with the same metrics on host B. Perhaps this -correlation occurs because they are running a load-balanced application. -If you enable this property, then anomalies will be reported when, for example, -CPU usage on host A is high and the value of CPU usage on host B is low. -That is to say, you'll see an anomaly when the CPU of host A is unusual given -the CPU of host B. - -NOTE: To use the `multivariate_by_fields` property, you must also specify -`by_field_name` in your detector. - --- - -`summary_count_field_name`:: - (string) If this property is specified, the data that is fed to the job is - expected to be pre-summarized. This property value is the name of the field - that contains the count of raw data points that have been summarized. The same - `summary_count_field_name` applies to all detectors in the job. -+ --- - -NOTE: The `summary_count_field_name` property cannot be used with the `metric` -function. - --- - -After you create a job, you cannot change the analysis configuration object; all -the properties are informational. - -[float] -[[ml-detectorconfig]] -==== Detector Configuration Objects - -Detector configuration objects specify which data fields a job analyzes. -They also specify which analytical functions are used. -You can specify multiple detectors for a job. -Each detector has the following properties: - -`by_field_name`:: - (string) The field used to split the data. - In particular, this property is used for analyzing the splits with respect to their own history. - It is used for finding unusual values in the context of the split. - -`detector_description`:: - (string) A description of the detector. For example, `Low event rate`. - -`detector_index`:: - (integer) A unique identifier for the detector. This identifier is based on - the order of the detectors in the `analysis_config`, starting at zero. You can - use this identifier when you want to update a specific detector. - -`exclude_frequent`:: - (string) Contains one of the following values: `all`, `none`, `by`, or `over`. - If set, frequent entities are excluded from influencing the anomaly results. - Entities can be considered frequent over time or frequent in a population. - If you are working with both over and by fields, then you can set `exclude_frequent` - to `all` for both fields, or to `by` or `over` for those specific fields. - -`field_name`:: - (string) The field that the detector uses in the function. If you use an event rate - function such as `count` or `rare`, do not specify this field. + -+ --- -NOTE: The `field_name` cannot contain double quotes or backslashes. - --- - -`function`:: - (string) The analysis function that is used. - For example, `count`, `rare`, `mean`, `min`, `max`, and `sum`. For more - information, see {stack-ov}/ml-functions.html[Function Reference]. - -`over_field_name`:: - (string) The field used to split the data. - In particular, this property is used for analyzing the splits with respect to - the history of all splits. It is used for finding unusual values in the - population of all splits. For more information, see - {stack-ov}/ml-configuring-pop.html[Performing population analysis]. - -`partition_field_name`:: - (string) The field used to segment the analysis. - When you use this property, you have completely independent baselines for each value of this field. - -`use_null`:: - (boolean) Defines whether a new series is used as the null series - when there is no value for the by or partition fields. The default value is `false`. - -`custom_rules`:: - (array) An array of custom rule objects, which enable customizing how the detector works. - For example, a rule may dictate to the detector conditions under which results should be skipped. - For more information see <>. + -+ --- -IMPORTANT: Field names are case sensitive, for example a field named 'Bytes' -is different from one named 'bytes'. - --- - -After you create a job, the only properties you can change in the detector -configuration object are the `detector_description` and the `custom_rules`; -all other properties are informational. - -[float] -[[ml-datadescription]] -==== Data Description Objects - -The data description defines the format of the input data when you send data to -the job by using the <> API. Note that when configure -a {dfeed}, these properties are automatically set. - -When data is received via the <> API, it is not stored -in {es}. Only the results for anomaly detection are retained. - -A data description object has the following properties: - -`format`:: - (string) Only `JSON` format is supported at this time. - -`time_field`:: - (string) The name of the field that contains the timestamp. - The default value is `time`. - -`time_format`:: - (string) The time format, which can be `epoch`, `epoch_ms`, or a custom pattern. - The default value is `epoch`, which refers to UNIX or Epoch time (the number of seconds - since 1 Jan 1970). - The value `epoch_ms` indicates that time is measured in milliseconds since the epoch. - The `epoch` and `epoch_ms` time formats accept either integer or real values. + -+ --- -NOTE: Custom patterns must conform to the Java `DateTimeFormatter` class. -When you use date-time formatting patterns, it is recommended that you provide -the full date, time and time zone. For example: `yyyy-MM-dd'T'HH:mm:ssX`. -If the pattern that you specify is not sufficient to produce a complete timestamp, -job creation fails. - --- - -[float] -[[ml-categorizationanalyzer]] -==== Categorization Analyzer - -The categorization analyzer specifies how the `categorization_field` is -interpreted by the categorization process. The syntax is very similar to that -used to define the `analyzer` in the <>. - -The `categorization_analyzer` field can be specified either as a string or as -an object. - -If it is a string it must refer to a <> or -one added by another plugin. - -If it is an object it has the following properties: - -`char_filter`:: - (array of strings or objects) One or more - <>. In addition to the built-in - character filters, other plugins can provide more character filters. This - property is optional. If it is not specified, no character filters are applied - prior to categorization. If you are customizing some other aspect of the - analyzer and you need to achieve the equivalent of `categorization_filters` - (which are not permitted when some other aspect of the analyzer is customized), - add them here as - <>. - -`tokenizer`:: - (string or object) The name or definition of the - <> to use after character filters are applied. - This property is compulsory if `categorization_analyzer` is specified as an - object. Machine learning provides a tokenizer called `ml_classic` that - tokenizes in the same way as the non-customizable tokenizer in older versions - of the product. If you want to use that tokenizer but change the character or - token filters, specify `"tokenizer": "ml_classic"` in your - `categorization_analyzer`. - -`filter`:: - (array of strings or objects) One or more - <>. In addition to the built-in token - filters, other plugins can provide more token filters. This property is - optional. If it is not specified, no token filters are applied prior to - categorization. - -If you omit the `categorization_analyzer`, the following default values are used: - -[source,console] --------------------------------------------------- -POST _ml/anomaly_detectors/_validate -{ - "analysis_config" : { - "categorization_analyzer" : { - "tokenizer" : "ml_classic", - "filter" : [ - { "type" : "stop", "stopwords": [ - "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", - "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", - "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", - "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", - "GMT", "UTC" - ] } - ] - }, - "categorization_field_name": "message", - "detectors" :[{ - "function":"count", - "by_field_name": "mlcategory" - }] - }, - "data_description" : { - } -} --------------------------------------------------- - -If you specify any part of the `categorization_analyzer`, however, any omitted -sub-properties are _not_ set to default values. - -If you are categorizing non-English messages in a language where words are -separated by spaces, you might get better results if you change the day or month -words in the stop token filter to the appropriate words in your language. If you -are categorizing messages in a language where words are not separated by spaces, -you must use a different tokenizer as well in order to get sensible -categorization results. - -It is important to be aware that analyzing for categorization of machine -generated log messages is a little different from tokenizing for search. -Features that work well for search, such as stemming, synonym substitution, and -lowercasing are likely to make the results of categorization worse. However, in -order for drill down from {ml} results to work correctly, the tokens that the -categorization analyzer produces must be similar to those produced by the search -analyzer. If they are sufficiently similar, when you search for the tokens that -the categorization analyzer produces then you find the original document that -the categorization field value came from. - -For more information, see -{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. - -[float] -[[ml-detector-custom-rule]] -==== Detector Custom Rule - -{stack-ov}/ml-rules.html[Custom rules] enable you to customize the way detectors -operate. - -A custom rule has the following properties: - -`actions`:: - (array) The set of actions to be triggered when the rule applies. - If more than one action is specified the effects of all actions are combined. - The available actions include: + - `skip_result`::: The result will not be created. This is the default value. - Unless you also specify `skip_model_update`, the model will be updated as - usual with the corresponding series value. - `skip_model_update`::: The value for that series will not be used to update - the model. Unless you also specify `skip_result`, the results will be created - as usual. This action is suitable when certain values are expected to be - consistently anomalous and they affect the model in a way that negatively - impacts the rest of the results. - -`scope`:: - (object) An optional scope of series where the rule applies. By default, the - scope includes all series. Scoping is allowed for any of the fields that are - also specified in `by_field_name`, `over_field_name`, or `partition_field_name`. - To add a scope for a field, add the field name as a key in the scope object and - set its value to an object with the following properties: -`filter_id`::: - (string) The id of the filter to be used. - -`filter_type`::: - (string) Either `include` (the rule applies for values in the filter) - or `exclude` (the rule applies for values not in the filter). Defaults - to `include`. - -`conditions`:: - (array) An optional array of numeric conditions when the rule applies. - Multiple conditions are combined together with a logical `AND`. -+ --- -NOTE: If your detector uses `lat_long`, `metric`, `rare`, or `freq_rare` -functions, you can only specify `conditions` that apply to `time`. - - -A condition has the following properties: - -`applies_to`::: - (string) Specifies the result property to which the condition applies. - The available options are `actual`, `typical`, `diff_from_typical`, `time`. -`operator`::: - (string) Specifies the condition operator. The available options are - `gt` (greater than), `gte` (greater than or equals), `lt` (less than) and `lte` (less than or equals). -`value`::: - (double) The value that is compared against the `applies_to` field using the `operator`. --- - -A rule is required to either have a non-empty scope or at least one condition. -For more examples see -{stack-ov}/ml-configuring-detector-custom-rules.html[Configuring Detector Custom Rules]. - -[float] -[[ml-apilimits]] -==== Analysis Limits - -Limits can be applied for the resources required to hold the mathematical models in memory. -These limits are approximate and can be set per job. They do not control the -memory used by other processes, for example the Elasticsearch Java processes. -If necessary, you can increase the limits after the job is created. - -The `analysis_limits` object has the following properties: - -`categorization_examples_limit`:: - (long) The maximum number of examples stored per category in memory and - in the results data store. The default value is 4. If you increase this value, - more examples are available, however it requires that you have more storage available. - If you set this value to `0`, no examples are stored. + -+ --- -NOTE: The `categorization_examples_limit` only applies to analysis that uses categorization. -For more information, see -{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. - --- - -`model_memory_limit`:: - (long or string) The approximate maximum amount of memory resources that are - required for analytical processing. Once this limit is approached, data pruning - becomes more aggressive. Upon exceeding this limit, new entities are not - modeled. The default value for jobs created in version 6.1 and later is `1024mb`. - This value will need to be increased for jobs that are expected to analyze high - cardinality fields, but the default is set to a relatively small size to ensure - that high resource usage is a conscious decision. The default value for jobs - created in versions earlier than 6.1 is `4096mb`. -+ --- -If you specify a number instead of a string, the units are assumed to be MiB. -Specifying a string is recommended for clarity. If you specify a byte size unit -of `b` or `kb` and the number does not equate to a discrete number of megabytes, -it is rounded down to the closest MiB. The minimum valid value is 1 MiB. If you -specify a value less than 1 MiB, an error occurs. For more information about -supported byte size units, see <>. - -If your `elasticsearch.yml` file contains an `xpack.ml.max_model_memory_limit` -setting, an error occurs when you try to create jobs that have -`model_memory_limit` values greater than that setting. For more information, -see <>. --- - -[float] -[[ml-apimodelplotconfig]] -==== Model Plot Config - -This advanced configuration option stores model information along with the -results. It provides a more detailed view into anomaly detection. - -WARNING: If you enable model plot it can add considerable overhead to the performance -of the system; it is not feasible for jobs with many entities. - -Model plot provides a simplified and indicative view of the model and its bounds. -It does not display complex features such as multivariate correlations or multimodal data. -As such, anomalies may occasionally be reported which cannot be seen in the model plot. - -Model plot config can be configured when the job is created or updated later. It must be -disabled if performance issues are experienced. - -The `model_plot_config` object has the following properties: - -`enabled`:: - (boolean) If true, enables calculation and storage of the model bounds for - each entity that is being analyzed. By default, this is not enabled. - -`terms`:: - experimental[] (string) Limits data collection to this comma separated list of - partition or by field values. If terms are not specified or it is an empty - string, no filtering is applied. For example, "CPU,NetworkIn,DiskWrites". - Wildcards are not supported. Only the specified `terms` can be viewed when - using the Single Metric Viewer. diff --git a/docs/reference/ml/anomaly-detection/apis/open-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/open-job.asciidoc index 5914ec502f104..3651834480f10 100644 --- a/docs/reference/ml/anomaly-detection/apis/open-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/open-job.asciidoc @@ -37,7 +37,8 @@ data is received. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {anomaly-job}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-open-job-request-body]] ==== {api-request-body-title} diff --git a/docs/reference/ml/anomaly-detection/apis/post-data.asciidoc b/docs/reference/ml/anomaly-detection/apis/post-data.asciidoc index a1e2120728acd..cfd3d4ca67fdc 100644 --- a/docs/reference/ml/anomaly-detection/apis/post-data.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/post-data.asciidoc @@ -53,7 +53,8 @@ or a comma-separated list. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the job. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-post-data-query-parms]] ==== {api-query-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/put-calendar-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-calendar-job.asciidoc index 7ba652b60a192..767a3d3d5bae5 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-calendar-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-calendar-job.asciidoc @@ -27,8 +27,8 @@ Adds an {anomaly-job} to a calendar. (Required, string) Identifier for the calendar. ``:: - (Required, string) An identifier for the {anomaly-jobs}. It can be a job - identifier, a group name, or a comma-separated list of jobs or groups. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-list] [[ml-put-calendar-job-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc index 899f8cfe5cd96..ca3b9d61ba7a1 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc @@ -97,10 +97,9 @@ not be set to `false` on any ML node. `script_fields`:: (Optional, object) Specifies scripts that evaluate custom expressions and - returns script fields to the {dfeed}. The - <> in a job can contain - functions that use these script fields. For more information, see - <>. + returns script fields to the {dfeed}. The detector configuration objects in a + job can contain functions that use these script fields. For more information, + see <>. `scroll_size`:: (Optional, unsigned integer) The `size` parameter that is used in {es} diff --git a/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc index 12fadac25d6d9..3ff63bf927757 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc @@ -25,7 +25,7 @@ Instantiates a filter. A {stack-ov}/ml-rules.html[filter] contains a list of strings. It can be used by one or more jobs. Specifically, filters are referenced in -the `custom_rules` property of <>. +the `custom_rules` property of detector configuration objects. [[ml-put-filter-path-parms]] ==== {api-path-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc index 69230160d2715..15f4ae3466968 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc @@ -32,64 +32,62 @@ a job directly to the `.ml-config` index using the {es} index API. If {es} ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the job. This identifier can contain - lowercase alphanumeric characters (a-z and 0-9), hyphens, and underscores. It - must start and end with alphanumeric characters. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-define] [[ml-put-job-request-body]] ==== {api-request-body-title} -`analysis_config`:: - (Required, object) The analysis configuration, which specifies how to analyze - the data. See <>. +`allow_lazy_open`:: +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-lazy-open] -`analysis_limits`:: - (Optional, object) Specifies runtime limits for the job. See - <>. +[[put-analysisconfig]]`analysis_config`:: +(Required, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=analysis-config] + +[[put-analysislimits]]`analysis_limits`:: +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=analysis-limits] `background_persist_interval`:: - (Optional, <>) Advanced configuration option. The time - between each periodic persistence of the model. See <>. +(Optional, <>) +include::{docdir}/ml/ml-shared.asciidoc[tag=background-persist-interval] -`custom_settings`:: - (Optional, object) Advanced configuration option. Contains custom meta data - about the job. See <>. +[[put-customsettings]]`custom_settings`:: +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-settings] -`data_description`:: - (Required, object) Describes the format of the input data. This object is - required, but it can be empty (`{}`). See - <>. +[[put-datadescription]]`data_description`:: +(Required, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=data-description] `description`:: (Optional, string) A description of the job. `groups`:: - (Optional, array of strings) A list of job groups. See <>. +(Optional, array of strings) +include::{docdir}/ml/ml-shared.asciidoc[tag=groups] `model_plot_config`:: - (Optional, object) Advanced configuration option. Specifies to store model - information along with the results. This adds overhead to the performance of - the system and is not feasible for jobs with many entities, see - <>. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-plot-config] `model_snapshot_retention_days`:: - (Optional, long) The time in days that model snapshots are retained for the - job. Older snapshots are deleted. The default value is `1`, which means - snapshots are retained for one day (twenty-four hours). +(Optional, long) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-snapshot-retention-days] `renormalization_window_days`:: - (Optional, long) Advanced configuration option. The period over which - adjustments to the score are applied, as new data is seen. See - <>. +(Optional, long) +include::{docdir}/ml/ml-shared.asciidoc[tag=renormalization-window-days] `results_index_name`:: - (Optional, string) A text string that affects the name of the {ml} results - index. The default value is `shared`, which generates an index named - `.ml-anomalies-shared`. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=results-index-name] `results_retention_days`:: - (Optional, long) Advanced configuration option. The number of days for which - job results are retained. See <>. +(Optional, long) +include::{docdir}/ml/ml-shared.asciidoc[tag=results-retention-days] [[ml-put-job-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/revert-snapshot.asciidoc b/docs/reference/ml/anomaly-detection/apis/revert-snapshot.asciidoc index f04db39e25eb8..d8cfc091b810f 100644 --- a/docs/reference/ml/anomaly-detection/apis/revert-snapshot.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/revert-snapshot.asciidoc @@ -36,7 +36,8 @@ Friday or a critical system failure. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the job. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: (Required, string) Identifier for the model snapshot. diff --git a/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc index 732f23202b1bf..d201d6cd093b2 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc @@ -92,10 +92,9 @@ parallel and close one when you are satisfied with the results of the other job. `script_fields`:: (Optional, object) Specifies scripts that evaluate custom expressions and - returns script fields to the {dfeed}. The - <> in a job can contain - functions that use these script fields. For more information, see - <>. + returns script fields to the {dfeed}. The detector configuration objects in a + job can contain functions that use these script fields. For more information, + see <>. `scroll_size`:: (Optional, unsigned integer) The `size` parameter that is used in {es} diff --git a/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc index 2af969dc993f1..09e6e03e57d5f 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc @@ -25,72 +25,94 @@ Updates certain properties of an {anomaly-job}. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {anomaly-job}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-update-job-request-body]] ==== {api-request-body-title} The following properties can be updated after the job is created: -[cols="<,<,<",options="header",] -|======================================================================= -|Name |Description |Requires Restart - -|`analysis_limits.model_memory_limit` |The approximate maximum amount of -memory resources required for analytical processing. See <>. You -can update the `analysis_limits` only while the job is closed. The -`model_memory_limit` property value cannot be decreased below the current usage. -| Yes - -|`background_persist_interval` |Advanced configuration option. The time between -each periodic persistence of the model. See <>. | Yes - -|`custom_settings` |Contains custom meta data about the job. | No - -|`description` |A description of the job. See <>. | No - -|`detectors` |An array of detector update objects. | No - -|`detector_index` |The identifier of the detector to update (integer).| No +`allow_lazy_open`:: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-lazy-open] ++ +-- +NOTE: If the job is open when you make the update, you must stop the {dfeed}, +close the job, then reopen the job and restart the {dfeed} for the changes to take effect. -|`detectors.description` |The new description for the detector.| No +-- -|`detectors.custom_rules` |The new list of <> -for the detector. | No +`detectors`:: +`custom_rules`::: +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules] +`description`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-description] +`detector_index`::: +(integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-index] + +[[update-analysislimits]]`analysis_limits`:: +`model_memory_limit`::: +(long or string) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-memory-limit] ++ +-- +NOTE: You can update the `analysis_limits` only while the job is closed. The +`model_memory_limit` property value cannot be decreased below the current usage. + +TIP: If the `memory_status` property in the +<> has a value of `hard_limit`, +this means that it was unable to process some data. You might want to re-run +the job with an increased `model_memory_limit`. -|`groups` |A list of job groups. See <>. | No +-- -|`model_plot_config.enabled` |If true, enables calculation and storage of the -model bounds for each entity that is being analyzed. -See <>. | No +`background_persist_interval`:: +(<>) +include::{docdir}/ml/ml-shared.asciidoc[tag=background-persist-interval] ++ +-- +NOTE: If the job is open when you make the update, you must stop the {dfeed}, +close the job, then reopen the job and restart the {dfeed} for the changes to take effect. -|`model_snapshot_retention_days` |The time in days that model snapshots are -retained for the job. See <>. | No +-- -|`renormalization_window_days` |Advanced configuration option. The period over -which adjustments to the score are applied, as new data is seen. -See <>. | Yes +[[update-customsettings]]`custom_settings`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-settings] -|`results_retention_days` |Advanced configuration option. The number of days -for which job results are retained. See <>. | No +`description`:: +(string) A description of the job. -|`allow_lazy_open` |Advanced configuration option. Whether to allow the job to be -opened when no {ml} node has sufficient capacity. See <>. | Yes +`groups`:: +(array of strings) +include::{docdir}/ml/ml-shared.asciidoc[tag=groups] -|======================================================================= +`model_plot_config`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-plot-config] -For those properties that have `Requires Restart` set to `Yes` in this table, -if the job is open when you make the update, you must stop the data feed, close -the job, then reopen the job and restart the data feed for the changes to take -effect. +`model_snapshot_retention_days`:: +(long) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-snapshot-retention-days] -[NOTE] +`renormalization_window_days`:: +(long) +include::{docdir}/ml/ml-shared.asciidoc[tag=renormalization-window-days] ++ -- -* If the `memory_status` property in the `model_size_stats` object has a value -of `hard_limit`, this means that it was unable to process some data. You might -want to re-run this job with an increased `model_memory_limit`. +NOTE: If the job is open when you make the update, you must stop the {dfeed}, +close the job, then reopen the job and restart the {dfeed} for the changes to take effect. + -- +`results_retention_days`:: +(long) +include::{docdir}/ml/ml-shared.asciidoc[tag=results-retention-days] + [[ml-update-job-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc index 1eb3e78e69ef4..10f7228fd9b2a 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc @@ -25,7 +25,8 @@ Updates certain properties of a snapshot. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {anomaly-job}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: (Required, string) Identifier for the model snapshot. diff --git a/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc b/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc index 74f7e717a0633..41d7e1e479c1c 100644 --- a/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc @@ -29,8 +29,7 @@ before you create an {anomaly-job}. [[ml-valid-detector-request-body]] ==== {api-request-body-title} -For a list of the properties that you can specify in the body of this API, -see <>. +include::{docdir}/ml/ml-shared.asciidoc[tag=detector] [[ml-valid-detector-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc index 8b094d36b2742..a741242922d20 100644 --- a/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc @@ -30,7 +30,7 @@ create the job. ==== {api-request-body-title} For a list of the properties that you can specify in the body of this API, -see <>. +see <>. [[ml-valid-job-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/categories.asciidoc b/docs/reference/ml/anomaly-detection/categories.asciidoc index a5e4d05541617..79c349509156b 100644 --- a/docs/reference/ml/anomaly-detection/categories.asciidoc +++ b/docs/reference/ml/anomaly-detection/categories.asciidoc @@ -144,7 +144,39 @@ language. The optional `categorization_analyzer` property allows even greater customization of how categorization interprets the categorization field value. It can refer to a built-in {es} analyzer or a combination of zero or more character filters, -a tokenizer, and zero or more token filters. +a tokenizer, and zero or more token filters. If you omit the +`categorization_analyzer`, the following default values are used: + +[source,console] +-------------------------------------------------- +POST _ml/anomaly_detectors/_validate +{ + "analysis_config" : { + "categorization_analyzer" : { + "tokenizer" : "ml_classic", + "filter" : [ + { "type" : "stop", "stopwords": [ + "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", + "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "GMT", "UTC" + ] } + ] + }, + "categorization_field_name": "message", + "detectors" :[{ + "function":"count", + "by_field_name": "mlcategory" + }] + }, + "data_description" : { + } +} +-------------------------------------------------- + +If you specify any part of the `categorization_analyzer`, however, any omitted +sub-properties are _not_ set to default values. The `ml_classic` tokenizer and the day and month stopword filter are more or less equivalent to the following analyzer, which is defined using only built-in {es} @@ -208,8 +240,22 @@ difference in behavior is that this custom analyzer does not include accented letters in tokens whereas the `ml_classic` tokenizer does, although that could be fixed by using more complex regular expressions. -For more information about the `categorization_analyzer` property, see -{ref}/ml-job-resource.html#ml-categorizationanalyzer[Categorization analyzer]. +If you are categorizing non-English messages in a language where words are +separated by spaces, you might get better results if you change the day or month +words in the stop token filter to the appropriate words in your language. If you +are categorizing messages in a language where words are not separated by spaces, +you must use a different tokenizer as well in order to get sensible +categorization results. + +It is important to be aware that analyzing for categorization of machine +generated log messages is a little different from tokenizing for search. +Features that work well for search, such as stemming, synonym substitution, and +lowercasing are likely to make the results of categorization worse. However, in +order for drill down from {ml} results to work correctly, the tokens that the +categorization analyzer produces must be similar to those produced by the search +analyzer. If they are sufficiently similar, when you search for the tokens that +the categorization analyzer produces then you find the original document that +the categorization field value came from. NOTE: To add the `categorization_analyzer` property in {kib}, you must use the **Edit JSON** tab and copy the `categorization_analyzer` object from one of the diff --git a/docs/reference/ml/anomaly-detection/functions.asciidoc b/docs/reference/ml/anomaly-detection/functions.asciidoc index d821a3ff4c023..a5e8d35af26bf 100644 --- a/docs/reference/ml/anomaly-detection/functions.asciidoc +++ b/docs/reference/ml/anomaly-detection/functions.asciidoc @@ -7,8 +7,7 @@ flexible ways to analyze data for anomalies. When you create {anomaly-jobs}, you specify one or more detectors, which define the type of analysis that needs to be done. If you are creating your job by -using {ml} APIs, you specify the functions in -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +using {ml} APIs, you specify the functions in detector configuration objects. If you are creating your job in {kib}, you specify the functions differently depending on whether you are creating single metric, multi-metric, or advanced jobs. diff --git a/docs/reference/ml/anomaly-detection/functions/count.asciidoc b/docs/reference/ml/anomaly-detection/functions/count.asciidoc index fe81fc5f59620..9310bf4fa0703 100644 --- a/docs/reference/ml/anomaly-detection/functions/count.asciidoc +++ b/docs/reference/ml/anomaly-detection/functions/count.asciidoc @@ -39,8 +39,8 @@ These functions support the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, -see {ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 1: Analyzing events with the count function [source,console] @@ -164,8 +164,8 @@ These functions support the following properties: * `by_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, -see {ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. For example, if you have the following number of events per bucket: @@ -233,8 +233,8 @@ These functions support the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, -see {ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 6: Analyzing users with the distinct_count function [source,console] diff --git a/docs/reference/ml/anomaly-detection/functions/geo.asciidoc b/docs/reference/ml/anomaly-detection/functions/geo.asciidoc index 20b8e6816ef7e..5eba44645f159 100644 --- a/docs/reference/ml/anomaly-detection/functions/geo.asciidoc +++ b/docs/reference/ml/anomaly-detection/functions/geo.asciidoc @@ -25,8 +25,8 @@ This function supports the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, -see {ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 1: Analyzing transactions with the lat_long function [source,console] diff --git a/docs/reference/ml/anomaly-detection/functions/info.asciidoc b/docs/reference/ml/anomaly-detection/functions/info.asciidoc index 18eb6d9f4e987..61913e539ce0b 100644 --- a/docs/reference/ml/anomaly-detection/functions/info.asciidoc +++ b/docs/reference/ml/anomaly-detection/functions/info.asciidoc @@ -28,8 +28,8 @@ These functions support the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 1: Analyzing subdomain strings with the info_content function [source,js] diff --git a/docs/reference/ml/anomaly-detection/functions/metric.asciidoc b/docs/reference/ml/anomaly-detection/functions/metric.asciidoc index cb44b61849a22..9ae29cf8a50d1 100644 --- a/docs/reference/ml/anomaly-detection/functions/metric.asciidoc +++ b/docs/reference/ml/anomaly-detection/functions/metric.asciidoc @@ -34,8 +34,8 @@ This function supports the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 1: Analyzing minimum transactions with the min function [source,js] @@ -69,8 +69,8 @@ This function supports the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 2: Analyzing maximum response times with the max function [source,js] @@ -132,7 +132,7 @@ These functions support the following properties: * `partition_field_name` (optional) For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 4: Analyzing response times with the median function [source,js] @@ -169,8 +169,8 @@ These functions support the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 5: Analyzing response times with the mean function [source,js] @@ -237,8 +237,8 @@ This function supports the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 8: Analyzing response times with the metric function [source,js] @@ -274,8 +274,8 @@ These functions support the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 9: Analyzing response times with the varp function [source,js] diff --git a/docs/reference/ml/anomaly-detection/functions/rare.asciidoc b/docs/reference/ml/anomaly-detection/functions/rare.asciidoc index 94931191a267b..f56b0fb8d076f 100644 --- a/docs/reference/ml/anomaly-detection/functions/rare.asciidoc +++ b/docs/reference/ml/anomaly-detection/functions/rare.asciidoc @@ -46,8 +46,8 @@ This function supports the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 1: Analyzing status codes with the rare function [source,js] @@ -105,8 +105,8 @@ This function supports the following properties: * `over_field_name` (required) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 3: Analyzing URI values in a population with the freq_rare function [source,js] diff --git a/docs/reference/ml/anomaly-detection/functions/sum.asciidoc b/docs/reference/ml/anomaly-detection/functions/sum.asciidoc index 260fc3f726c53..387769c80f333 100644 --- a/docs/reference/ml/anomaly-detection/functions/sum.asciidoc +++ b/docs/reference/ml/anomaly-detection/functions/sum.asciidoc @@ -35,8 +35,8 @@ These functions support the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 1: Analyzing total expenses with the sum function [source,js] @@ -91,8 +91,8 @@ These functions support the following properties: * `by_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. NOTE: Population analysis (that is to say, use of the `over_field_name` property) is not applicable for this function. diff --git a/docs/reference/ml/anomaly-detection/functions/time.asciidoc b/docs/reference/ml/anomaly-detection/functions/time.asciidoc index 22cab11151d1a..cdf11cba4470d 100644 --- a/docs/reference/ml/anomaly-detection/functions/time.asciidoc +++ b/docs/reference/ml/anomaly-detection/functions/time.asciidoc @@ -53,8 +53,8 @@ This function supports the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 1: Analyzing events with the time_of_day function [source,js] @@ -84,8 +84,8 @@ This function supports the following properties: * `over_field_name` (optional) * `partition_field_name` (optional) -For more information about those properties, see -{ref}/ml-job-resource.html#ml-detectorconfig[Detector configuration objects]. +For more information about those properties, see the +{ref}/ml-put-job.html#ml-put-job-request-body[create {anomaly-jobs} API]. .Example 2: Analyzing events with the time_of_week function [source,js] diff --git a/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc index 7816931161bb4..3f27c91fd016f 100644 --- a/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc @@ -26,7 +26,8 @@ information, see <> and <>. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {dfanalytics-job} you want to delete. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics] [[ml-delete-dfanalytics-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/df-analytics/apis/explain-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/explain-dfanalytics.asciidoc index c9ee565e9b2c5..fb867b53ce165 100644 --- a/docs/reference/ml/df-analytics/apis/explain-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/explain-dfanalytics.asciidoc @@ -43,10 +43,8 @@ about either an existing {dfanalytics-job} or one that has not been created yet. ==== {api-path-parms-title} ``:: - (Optional, string) A numerical character string that uniquely identifies the existing - {dfanalytics-job} to explain. This identifier can contain lowercase alphanumeric - characters (a-z and 0-9), hyphens, and underscores. It must start and end with - alphanumeric characters. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics] [[ml-explain-dfanalytics-request-body]] ==== {api-request-body-title} diff --git a/docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc b/docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc index ab065e2622da6..dcdb66689936f 100644 --- a/docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc +++ b/docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc @@ -36,27 +36,16 @@ information, see <> and <>. ==== {api-path-parms-title} ``:: - (Optional, string)Identifier for the {dfanalytics-job}. If you do not specify - one of these options, the API returns information for the first hundred - {dfanalytics-jobs}. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics-default] [[ml-get-dfanalytics-stats-query-params]] ==== {api-query-parms-title} `allow_no_match`:: - (Optional, boolean) Specifies what to do when the request: -+ --- -* Contains wildcard expressions and there are no {dfanalytics-jobs} that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns an empty `data_frame_analytics` array -when there are no matches and the subset of results when there are partial -matches. If this parameter is `false`, the request returns a `404` status code -when there are no matches or only partial matches. --- +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-match] `from`:: (Optional, integer) Skips the specified number of {dfanalytics-jobs}. The diff --git a/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc index ddc8f35f280e6..5b0987e41c4bc 100644 --- a/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc @@ -86,11 +86,8 @@ single number. For example, in case of age ranges, you can model the values as ==== {api-path-parms-title} ``:: - (Required, string) A numerical character string that uniquely identifies the - {dfanalytics-job}. This identifier can contain lowercase alphanumeric - characters (a-z and 0-9), hyphens, and underscores. It must start and end with - alphanumeric characters. - +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics-define] [[ml-put-dfanalytics-request-body]] ==== {api-request-body-title} diff --git a/docs/reference/ml/df-analytics/apis/start-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/start-dfanalytics.asciidoc index 9ffbfc3d9c23e..ba8b4169034a4 100644 --- a/docs/reference/ml/df-analytics/apis/start-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/start-dfanalytics.asciidoc @@ -29,9 +29,8 @@ more information, see <> and <>. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {dfanalytics-job}. This identifier can - contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and - underscores. It must start and end with alphanumeric characters. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics-define] [[ml-start-dfanalytics-query-params]] ==== {api-query-parms-title} diff --git a/docs/reference/ml/df-analytics/apis/stop-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/stop-dfanalytics.asciidoc index 8c9f705062cb6..ce3a932f1b07d 100644 --- a/docs/reference/ml/df-analytics/apis/stop-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/stop-dfanalytics.asciidoc @@ -42,9 +42,8 @@ stop all {dfanalytics-job} by using _all or by specifying * as the ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {dfanalytics-job}. This identifier can - contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and - underscores. It must start and end with alphanumeric characters. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics-define] [[ml-stop-dfanalytics-query-params]] ==== {api-query-parms-title} diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index cac244d7135cf..11e062796afa6 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -1,3 +1,306 @@ +tag::allow-lazy-open[] +Advanced configuration option. Specifies whether this job can open when there is +insufficient {ml} node capacity for it to be immediately assigned to a node. The +default value is `false`; if a {ml} node with capacity to run the job cannot immediately be found, the <> returns an +error. However, this is also subject to the cluster-wide +`xpack.ml.max_lazy_ml_nodes` setting; see <>. If this +option is set to `true`, the <> does not +return an error and the job waits in the `opening` state until sufficient {ml} +node capacity is available. +end::allow-lazy-open[] + +tag::allow-no-jobs[] +Specifies what to do when the request: ++ +-- +* Contains wildcard expressions and there are no jobs that match. +* Contains the `_all` string or no identifiers and there are no matches. +* Contains wildcard expressions and there are only partial matches. + +The default value is `true`, which returns an empty `jobs` array +when there are no matches and the subset of results when there are partial +matches. If this parameter is `false`, the request returns a `404` status code +when there are no matches or only partial matches. +-- +end::allow-no-jobs[] + +tag::allow-no-match[] + Specifies what to do when the request: ++ +-- +* Contains wildcard expressions and there are no {dfanalytics-jobs} that match. +* Contains the `_all` string or no identifiers and there are no matches. +* Contains wildcard expressions and there are only partial matches. + +The default value is `true`, which returns an empty `data_frame_analytics` array +when there are no matches and the subset of results when there are partial +matches. If this parameter is `false`, the request returns a `404` status code +when there are no matches or only partial matches. +-- +end::allow-no-match[] + +tag::analysis-config[] +The analysis configuration, which specifies how to analyze the data. +After you create a job, you cannot change the analysis configuration; all +the properties are informational. An analysis configuration object has the following properties: + +`bucket_span`::: +(<>) +include::{docdir}/ml/ml-shared.asciidoc[tag=bucket-span] + +`categorization_field_name`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-field-name] + +`categorization_filters`::: +(array of strings) +include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-filters] + +`categorization_analyzer`::: +(object or string) +include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-analyzer] + +`detectors`::: +(array) An array of detector configuration objects. Detector configuration +objects specify which data fields a job analyzes. They also specify which +analytical functions are used. You can specify multiple detectors for a job. +include::{docdir}/ml/ml-shared.asciidoc[tag=detector] ++ +-- +NOTE: If the `detectors` array does not contain at least one detector, +no analysis can occur and an error is returned. + +-- + +`influencers`::: +(array of strings) +include::{docdir}/ml/ml-shared.asciidoc[tag=influencers] + +`latency`::: +(time units) +include::{docdir}/ml/ml-shared.asciidoc[tag=latency] + +`multivariate_by_fields`::: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=multivariate-by-fields] + +`summary_count_field_name`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=summary-count-field-name] + +end::analysis-config[] + +tag::analysis-limits[] +Limits can be applied for the resources required to hold the mathematical models +in memory. These limits are approximate and can be set per job. They do not +control the memory used by other processes, for example the {es} Java +processes. If necessary, you can increase the limits after the job is created. +The `analysis_limits` object has the following properties: + +`categorization_examples_limit`::: +(long) +include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-examples-limit] + +`model_memory_limit`::: +(long or string) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-memory-limit] +end::analysis-limits[] + +tag::background-persist-interval[] +Advanced configuration option. The time between each periodic persistence of the +model. The default value is a randomized value between 3 to 4 hours, which +avoids all jobs persisting at exactly the same time. The smallest allowed value +is 1 hour. ++ +-- +TIP: For very large models (several GB), persistence could take 10-20 minutes, +so do not set the `background_persist_interval` value too low. + +-- +end::background-persist-interval[] + +tag::bucket-span[] +The size of the interval that the analysis is aggregated into, typically between +`5m` and `1h`. The default value is `5m`. For more information about time units, +see <>. +end::bucket-span[] + +tag::by-field-name[] +The field used to split the data. In particular, this property is used for +analyzing the splits with respect to their own history. It is used for finding +unusual values in the context of the split. +end::by-field-name[] + +tag::categorization-analyzer[] +If `categorization_field_name` is specified, you can also define the analyzer +that is used to interpret the categorization field. This property cannot be used +at the same time as `categorization_filters`. The categorization analyzer +specifies how the `categorization_field` is interpreted by the categorization +process. The syntax is very similar to that used to define the `analyzer` in the +<>. For more information, see +{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. ++ +-- +The `categorization_analyzer` field can be specified either as a string or as an +object. If it is a string it must refer to a +<> or one added by another plugin. If it +is an object it has the following properties: +-- + +`char_filter`:::: +(array of strings or objects) +include::{docdir}/ml/ml-shared.asciidoc[tag=char-filter] + +`tokenizer`:::: +(string or object) +include::{docdir}/ml/ml-shared.asciidoc[tag=tokenizer] + +`filter`:::: +(array of strings or objects) +include::{docdir}/ml/ml-shared.asciidoc[tag=filter] +end::categorization-analyzer[] + +tag::categorization-examples-limit[] +The maximum number of examples stored per category in memory and in the results +data store. The default value is 4. If you increase this value, more examples +are available, however it requires that you have more storage available. If you +set this value to `0`, no examples are stored. ++ +-- +NOTE: The `categorization_examples_limit` only applies to analysis that uses +categorization. For more information, see +{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. + +-- +end::categorization-examples-limit[] + +tag::categorization-field-name[] +If this property is specified, the values of the specified field will be +categorized. The resulting categories must be used in a detector by setting +`by_field_name`, `over_field_name`, or `partition_field_name` to the keyword +`mlcategory`. For more information, see +{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. +end::categorization-field-name[] + +tag::categorization-filters[] +If `categorization_field_name` is specified, you can also define optional +filters. This property expects an array of regular expressions. The expressions +are used to filter out matching sequences from the categorization field values. +You can use this functionality to fine tune the categorization by excluding sequences from consideration when categories are defined. For example, you can exclude SQL statements that appear in your log files. For more information, see +{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. This +property cannot be used at the same time as `categorization_analyzer`. If you +only want to define simple regular expression filters that are applied prior to +tokenization, setting this property is the easiest method. If you also want to +customize the tokenizer or post-tokenization filtering, use the +`categorization_analyzer` property instead and include the filters as +`pattern_replace` character filters. The effect is exactly the same. +end::categorization-filters[] + +tag::char-filter[] +One or more <>. In addition to the +built-in character filters, other plugins can provide more character filters. +This property is optional. If it is not specified, no character filters are +applied prior to categorization. If you are customizing some other aspect of the +analyzer and you need to achieve the equivalent of `categorization_filters` +(which are not permitted when some other aspect of the analyzer is customized), +add them here as +<>. +end::char-filter[] + +tag::custom-rules[] +An array of custom rule objects, which enable you to customize the way detectors +operate. For example, a rule may dictate to the detector conditions under which +results should be skipped. For more examples, see +{stack-ov}/ml-configuring-detector-custom-rules.html[Configuring detector custom rules]. +A custom rule has the following properties: ++ +-- +`actions`:: +(array) The set of actions to be triggered when the rule applies. If +more than one action is specified the effects of all actions are combined. The +available actions include: + +* `skip_result`: The result will not be created. This is the default value. +Unless you also specify `skip_model_update`, the model will be updated as usual +with the corresponding series value. +* `skip_model_update`: The value for that series will not be used to update the +model. Unless you also specify `skip_result`, the results will be created as +usual. This action is suitable when certain values are expected to be +consistently anomalous and they affect the model in a way that negatively +impacts the rest of the results. + +`scope`:: +(object) An optional scope of series where the rule applies. A rule must either +have a non-empty scope or at least one condition. By default, the scope includes +all series. Scoping is allowed for any of the fields that are also specified in +`by_field_name`, `over_field_name`, or `partition_field_name`. To add a scope +for a field, add the field name as a key in the scope object and set its value +to an object with the following properties: + +`filter_id`::: +(string) The id of the filter to be used. + +`filter_type`::: +(string) Either `include` (the rule applies for values in the filter) or +`exclude` (the rule applies for values not in the filter). Defaults to `include`. + +`conditions`:: +(array) An optional array of numeric conditions when the rule applies. A rule +must either have a non-empty scope or at least one condition. Multiple +conditions are combined together with a logical `AND`. A condition has the +following properties: + +`applies_to`::: +(string) Specifies the result property to which the condition applies. The +available options are `actual`, `typical`, `diff_from_typical`, `time`. + +`operator`::: +(string) Specifies the condition operator. The available options are `gt` +(greater than), `gte` (greater than or equals), `lt` (less than) and `lte` (less +than or equals). + +`value`::: +(double) The value that is compared against the `applies_to` field using the +`operator`. +-- ++ +-- +NOTE: If your detector uses `lat_long`, `metric`, `rare`, or `freq_rare` +functions, you can only specify `conditions` that apply to `time`. + +-- +end::custom-rules[] + +tag::custom-settings[] +Advanced configuration option. Contains custom meta data about the job. For +example, it can contain custom URL information as shown in +{stack-ov}/ml-configuring-url.html[Adding custom URLs to {ml} results]. +end::custom-settings[] + +tag::data-description[] +The data description defines the format of the input data when you send data to +the job by using the <> API. Note that when configure +a {dfeed}, these properties are automatically set. ++ +-- +When data is received via the <> API, it is not stored +in {es}. Only the results for {anomaly-detect} are retained. + +A data description object has the following properties: + +`format`::: + (string) Only `JSON` format is supported at this time. + +`time_field`::: + (string) The name of the field that contains the timestamp. + The default value is `time`. + +`time_format`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=time-format] +-- +end::data-description[] + tag::dependent_variable[] `dependent_variable`:: (Required, string) Defines which field of the document is to be predicted. @@ -7,6 +310,70 @@ that document will not be used for training, but a prediction with the trained model will be generated for it. It is also known as continuous target variable. end::dependent_variable[] +tag::detector-description[] +A description of the detector. For example, `Low event rate`. +end::detector-description[] + +tag::detector-field-name[] +The field that the detector uses in the function. If you use an event rate +function such as `count` or `rare`, do not specify this field. ++ +-- +NOTE: The `field_name` cannot contain double quotes or backslashes. + +-- +end::detector-field-name[] + +tag::detector-index[] +A unique identifier for the detector. This identifier is based on the order of +the detectors in the `analysis_config`, starting at zero. You can use this +identifier when you want to update a specific detector. +end::detector-index[] + +tag::detector[] +A detector has the following properties: + +`by_field_name`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=by-field-name] + +`custom_rules`:::: +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules] + +`detector_description`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-description] + +`detector_index`:::: +(integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-index] + +`exclude_frequent`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=exclude-frequent] + +`field_name`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-field-name] + +`function`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=function] + +`over_field_name`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=over-field-name] + +`partition_field_name`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=partition-field-name] + +`use_null`:::: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=use-null] + +end::detector[] tag::eta[] `eta`:: @@ -17,6 +384,13 @@ https://en.wikipedia.org/wiki/Gradient_boosting#Shrinkage[this wiki article] about shrinkage. end::eta[] +tag::exclude-frequent[] +Contains one of the following values: `all`, `none`, `by`, or `over`. If set, +frequent entities are excluded from influencing the anomaly results. Entities +can be considered frequent over time or frequent in a population. If you are +working with both over and by fields, then you can set `exclude_frequent` to +`all` for both fields, or to `by` or `over` for those specific fields. +end::exclude-frequent[] tag::feature_bag_fraction[] `feature_bag_fraction`:: @@ -24,6 +398,18 @@ tag::feature_bag_fraction[] selecting a random bag for each candidate split. end::feature_bag_fraction[] +tag::filter[] +One or more <>. In addition to the built-in +token filters, other plugins can provide more token filters. This property is +optional. If it is not specified, no token filters are applied prior to +categorization. +end::filter[] + +tag::function[] +The analysis function that is used. For example, `count`, `rare`, `mean`, `min`, +`max`, and `sum`. For more information, see +{stack-ov}/ml-functions.html[Function reference]. +end::function[] tag::gamma[] `gamma`:: @@ -34,8 +420,67 @@ prefer smaller trees. The smaller this parameter the larger individual trees will be and the longer train will take. end::gamma[] +tag::groups[] +A list of job groups. A job can belong to no groups or many. +end::groups[] + +tag::influencers[] +A comma separated list of influencer field names. Typically these can be the by, +over, or partition fields that are used in the detector configuration. You might +also want to use a field name that is not specifically named in a detector, but +is available as part of the input data. When you use multiple detectors, the use +of influencers is recommended as it aggregates results for each influencer entity. +end::influencers[] + +tag::job-id-anomaly-detection[] +Identifier for the {anomaly-job}. +end::job-id-anomaly-detection[] + +tag::job-id-data-frame-analytics[] +Identifier for the {dfanalytics-job}. +end::job-id-data-frame-analytics[] + +tag::job-id-anomaly-detection-default[] +Identifier for the {anomaly-job}. It can be a job identifier, a group name, or a wildcard expression. If you do not specify one of these options, the API returns information for all {anomaly-jobs}. +end::job-id-anomaly-detection-default[] + +tag::job-id-data-frame-analytics-default[] +Identifier for the {dfanalytics-job}. If you do not specify this option, the API +returns information for the first hundred {dfanalytics-jobs}. +end::job-id-data-frame-analytics-default[] + +tag::job-id-anomaly-detection-list[] +An identifier for the {anomaly-jobs}. It can be a job +identifier, a group name, or a comma-separated list of jobs or groups. +end::job-id-anomaly-detection-list[] + +tag::job-id-anomaly-detection-wildcard[] +Identifier for the {anomaly-job}. It can be a job identifier, a group name, or a wildcard expression. +end::job-id-anomaly-detection-wildcard[] + +tag::job-id-anomaly-detection-wildcard-list[] +Identifier for the {anomaly-job}. It can be a job identifier, a group name, a +comma-separated list of jobs or groups, or a wildcard expression. +end::job-id-anomaly-detection-wildcard-list[] + +tag::job-id-anomaly-detection-define[] +Identifier for the {anomaly-job}. This identifier can contain lowercase alphanumeric +characters (a-z and 0-9), hyphens, and underscores. It must start and end with +alphanumeric characters. +end::job-id-anomaly-detection-define[] + +tag::job-id-data-frame-analytics-define[] +Identifier for the {dfanalytics-job}. This identifier can contain lowercase +alphanumeric characters (a-z and 0-9), hyphens, and underscores. It must start +and end with alphanumeric characters. +end::job-id-data-frame-analytics-define[] -tag::lambda[] +tag::jobs-stats-anomaly-detection[] +An array of {anomaly-job} statistics objects. +For more information, see <>. +end::jobs-stats-anomaly-detection[] + +tag::lambda[] `lambda`:: (Optional, double) Regularization parameter to prevent overfitting on the training dataset. Multiplies an L2 regularisation term which applies to leaf @@ -46,6 +491,16 @@ relevant relationships between the features and the {depvar}. The smaller this parameter the larger individual trees will be and the longer train will take. end::lambda[] +tag::latency[] +The size of the window in which to expect data that is out of time order. The +default value is 0 (no latency). If you specify a non-zero value, it must be greater than or equal to one second. For more information about time units, see <>. ++ +-- +NOTE: Latency is only applicable when you send data by using +the <> API. + +-- +end::latency[] tag::maximum_number_trees[] `maximum_number_trees`:: @@ -53,6 +508,106 @@ tag::maximum_number_trees[] to contain. The maximum value is 2000. end::maximum_number_trees[] +tag::model-memory-limit[] +The approximate maximum amount of memory resources that are required for +analytical processing. Once this limit is approached, data pruning becomes +more aggressive. Upon exceeding this limit, new entities are not modeled. The +default value for jobs created in version 6.1 and later is `1024mb`. +This value will need to be increased for jobs that are expected to analyze high +cardinality fields, but the default is set to a relatively small size to ensure +that high resource usage is a conscious decision. The default value for jobs +created in versions earlier than 6.1 is `4096mb`. ++ +-- +If you specify a number instead of a string, the units are assumed to be MiB. +Specifying a string is recommended for clarity. If you specify a byte size unit +of `b` or `kb` and the number does not equate to a discrete number of megabytes, +it is rounded down to the closest MiB. The minimum valid value is 1 MiB. If you +specify a value less than 1 MiB, an error occurs. For more information about +supported byte size units, see <>. + +If your `elasticsearch.yml` file contains an `xpack.ml.max_model_memory_limit` +setting, an error occurs when you try to create jobs that have +`model_memory_limit` values greater than that setting. For more information, +see <>. +-- +end::model-memory-limit[] + +tag::model-plot-config[] +This advanced configuration option stores model information along with the +results. It provides a more detailed view into {anomaly-detect}. ++ +-- +WARNING: If you enable model plot it can add considerable overhead to the performance +of the system; it is not feasible for jobs with many entities. + +Model plot provides a simplified and indicative view of the model and its bounds. +It does not display complex features such as multivariate correlations or multimodal data. +As such, anomalies may occasionally be reported which cannot be seen in the model plot. + +Model plot config can be configured when the job is created or updated later. It must be +disabled if performance issues are experienced. + +The `model_plot_config` object has the following properties: + +`enabled`::: +(boolean) If true, enables calculation and storage of the model bounds for +each entity that is being analyzed. By default, this is not enabled. + +`terms`::: +experimental[] (string) Limits data collection to this comma separated list of +partition or by field values. If terms are not specified or it is an empty +string, no filtering is applied. For example, "CPU,NetworkIn,DiskWrites". +Wildcards are not supported. Only the specified `terms` can be viewed when +using the Single Metric Viewer. +-- +end::model-plot-config[] + +tag::model-snapshot-id[] +A numerical character string that uniquely identifies the model snapshot. For +example, `1491007364`. For more information about model snapshots, see +<>. +end::model-snapshot-id[] + +tag::model-snapshot-retention-days[] +The time in days that model snapshots are retained for the job. Older snapshots +are deleted. The default value is `1`, which means snapshots are retained for +one day (twenty-four hours). +end::model-snapshot-retention-days[] + +tag::multivariate-by-fields[] +This functionality is reserved for internal use. It is not supported for use in +customer environments and is not subject to the support SLA of official GA +features. ++ +-- +If set to `true`, the analysis will automatically find correlations between +metrics for a given `by` field value and report anomalies when those +correlations cease to hold. For example, suppose CPU and memory usage on host A +is usually highly correlated with the same metrics on host B. Perhaps this +correlation occurs because they are running a load-balanced application. +If you enable this property, then anomalies will be reported when, for example, +CPU usage on host A is high and the value of CPU usage on host B is low. That +is to say, you'll see an anomaly when the CPU of host A is unusual given +the CPU of host B. + +NOTE: To use the `multivariate_by_fields` property, you must also specify +`by_field_name` in your detector. + +-- +end::multivariate-by-fields[] + +tag::over-field-name[] +The field used to split the data. In particular, this property is used for +analyzing the splits with respect to the history of all splits. It is used for +finding unusual values in the population of all splits. For more information, +see {stack-ov}/ml-configuring-pop.html[Performing population analysis]. +end::over-field-name[] + +tag::partition-field-name[] +The field used to segment the analysis. When you use this property, you have +completely independent baselines for each value of this field. +end::partition-field-name[] tag::prediction_field_name[] `prediction_field_name`:: @@ -60,6 +615,63 @@ tag::prediction_field_name[] Defaults to `_prediction`. end::prediction_field_name[] +tag::renormalization-window-days[] +Advanced configuration option. The period over which adjustments to the score +are applied, as new data is seen. The default value is the longer of 30 days or +100 `bucket_spans`. +end::renormalization-window-days[] + +tag::results-index-name[] +A text string that affects the name of the {ml} results index. The default value +is `shared`, which generates an index named `.ml-anomalies-shared`. +end::results-index-name[] + +tag::results-retention-days[] +Advanced configuration option. The number of days for which job results are +retained. Once per day at 00:30 (server time), results older than this period +are deleted from {es}. The default value is null, which means results are +retained. +end::results-retention-days[] + +tag::summary-count-field-name[] +If this property is specified, the data that is fed to the job is expected to be +pre-summarized. This property value is the name of the field that contains the +count of raw data points that have been summarized. The same +`summary_count_field_name` applies to all detectors in the job. ++ +-- +NOTE: The `summary_count_field_name` property cannot be used with the `metric` +function. + +-- +end::summary-count-field-name[] + +tag::time-format[] +The time format, which can be `epoch`, `epoch_ms`, or a custom pattern. The +default value is `epoch`, which refers to UNIX or Epoch time (the number of +seconds since 1 Jan 1970). The value `epoch_ms` indicates that time is measured +in milliseconds since the epoch. The `epoch` and `epoch_ms` time formats accept +either integer or real values. + ++ +-- +NOTE: Custom patterns must conform to the Java `DateTimeFormatter` class. +When you use date-time formatting patterns, it is recommended that you provide +the full date, time and time zone. For example: `yyyy-MM-dd'T'HH:mm:ssX`. +If the pattern that you specify is not sufficient to produce a complete timestamp, +job creation fails. + +-- +end::time-format[] + +tag::tokenizer[] +The name or definition of the <> to use after +character filters are applied. This property is compulsory if +`categorization_analyzer` is specified as an object. Machine learning provides a +tokenizer called `ml_classic` that tokenizes in the same way as the +non-customizable tokenizer in older versions of the product. If you want to use +that tokenizer but change the character or token filters, specify +`"tokenizer": "ml_classic"` in your `categorization_analyzer`. +end::tokenizer[] tag::training_percent[] `training_percent`:: @@ -67,4 +679,9 @@ tag::training_percent[] be used for training. Documents that are ignored by the analysis (for example those that contain arrays) won’t be included in the calculation for used percentage. Defaults to `100`. -end::training_percent[] \ No newline at end of file +end::training_percent[] + +tag::use-null[] +Defines whether a new series is used as the null series when there is no value +for the by or partition fields. The default value is `false`. +end::use-null[] diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 2ea8f6029849d..26694e9aa05ab 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -1046,3 +1046,12 @@ See <>. === Rollup job configuration See <>. + +[role="exclude",id="ml-job-resource"] +=== Job resources + +This page was deleted. +[[ml-analysisconfig]] +See the details in +[[ml-apimodelplotconfig]] +<>, <>, and <>. \ No newline at end of file diff --git a/docs/reference/rest-api/defs.asciidoc b/docs/reference/rest-api/defs.asciidoc index 75e496903f59d..ec1a5a0e4154f 100644 --- a/docs/reference/rest-api/defs.asciidoc +++ b/docs/reference/rest-api/defs.asciidoc @@ -9,7 +9,6 @@ These resource definitions are used in APIs related to {ml-features} and * <> * <> * <> -* <> * <> * <> * <> @@ -19,7 +18,6 @@ These resource definitions are used in APIs related to {ml-features} and include::{es-repo-dir}/ml/anomaly-detection/apis/datafeedresource.asciidoc[] include::{es-repo-dir}/ml/df-analytics/apis/dfanalyticsresources.asciidoc[] include::{es-repo-dir}/ml/df-analytics/apis/evaluateresources.asciidoc[] -include::{es-repo-dir}/ml/anomaly-detection/apis/jobresource.asciidoc[] include::{es-repo-dir}/ml/anomaly-detection/apis/jobcounts.asciidoc[] include::{es-repo-dir}/ml/anomaly-detection/apis/snapshotresource.asciidoc[] include::{xes-repo-dir}/rest-api/security/role-mapping-resources.asciidoc[] diff --git a/docs/reference/settings/ml-settings.asciidoc b/docs/reference/settings/ml-settings.asciidoc index 52d0d8eb28b24..8829a328f7955 100644 --- a/docs/reference/settings/ml-settings.asciidoc +++ b/docs/reference/settings/ml-settings.asciidoc @@ -81,7 +81,7 @@ The maximum `model_memory_limit` property value that can be set for any job on this node. If you try to create a job with a `model_memory_limit` property value that is greater than this setting value, an error occurs. Existing jobs are not affected when you update this setting. For more information about the -`model_memory_limit` property, see <>. +`model_memory_limit` property, see <>. `xpack.ml.max_open_jobs` (<>):: The maximum number of jobs that can run simultaneously on a node. Defaults to From 71373d048c8a595c9b093d0a41e8c67c2f8d72d1 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Fri, 6 Dec 2019 15:54:30 -0800 Subject: [PATCH 107/686] Disable repo configuration for rpm based systems (#49893) This commit changes the recommended repository file for rpm based systems to be disabled by default. This is a safer practice so upgrades of the system do no accidentally upgrade elasticsearch itself. closes #30660 --- docs/reference/setup/install/rpm.asciidoc | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/reference/setup/install/rpm.asciidoc b/docs/reference/setup/install/rpm.asciidoc index 4108756286eed..c6239bc576054 100644 --- a/docs/reference/setup/install/rpm.asciidoc +++ b/docs/reference/setup/install/rpm.asciidoc @@ -48,12 +48,12 @@ ifeval::["{release-state}"=="released"] ["source","sh",subs="attributes,callouts"] -------------------------------------------------- -[elasticsearch-{major-version}] +[elasticsearch] name=Elasticsearch repository for {major-version} packages baseurl=https://artifacts.elastic.co/packages/{major-version}/yum gpgcheck=1 gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch -enabled=1 +enabled=0 autorefresh=1 type=rpm-md -------------------------------------------------- @@ -64,12 +64,12 @@ ifeval::["{release-state}"=="prerelease"] ["source","sh",subs="attributes,callouts"] -------------------------------------------------- -[elasticsearch-{major-version}] +[elasticsearch] name=Elasticsearch repository for {major-version} packages baseurl=https://artifacts.elastic.co/packages/{major-version}-prerelease/yum gpgcheck=1 gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch -enabled=1 +enabled=0 autorefresh=1 type=rpm-md -------------------------------------------------- @@ -80,14 +80,23 @@ And your repository is ready for use. You can now install Elasticsearch with one [source,sh] -------------------------------------------------- -sudo yum install elasticsearch <1> -sudo dnf install elasticsearch <2> -sudo zypper install elasticsearch <3> +sudo yum install --enablerepo=elasticsearch elasticsearch <1> +sudo dnf install --enablerepo=elasticsearch elasticsearch <2> +sudo zypper modifyrepo --enable elasticsearch && \ + sudo zypper install elasticsearch; \ + sudo zypper modifyrepo --disable elasticsearch <3> -------------------------------------------------- <1> Use `yum` on CentOS and older Red Hat based distributions. <2> Use `dnf` on Fedora and other newer Red Hat distributions. <3> Use `zypper` on OpenSUSE based distributions +[NOTE] +================================================== + +The configured repository is disabled by default. This eliminates the possibility of accidentally +upgrading `elasticsearch` when upgrading the rest of the system. Each install or upgrade command +must explicitly enable the repository as indicated in the sample commands above. + endif::[] ifeval::["{release-state}"!="unreleased"] From 3c36cd987a8b2627279adcfc06108d560a90f6b9 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Fri, 6 Dec 2019 17:43:12 -0800 Subject: [PATCH 108/686] Fix incorrect use of multiline NOTE in rpm docs (#49962) This was a copy/paste error from #49893. This commit converts the NOTE to use inline style instead of one needing closing linebreak. --- docs/reference/setup/install/rpm.asciidoc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/reference/setup/install/rpm.asciidoc b/docs/reference/setup/install/rpm.asciidoc index c6239bc576054..f1601f95b1773 100644 --- a/docs/reference/setup/install/rpm.asciidoc +++ b/docs/reference/setup/install/rpm.asciidoc @@ -90,10 +90,7 @@ sudo zypper modifyrepo --enable elasticsearch && \ <2> Use `dnf` on Fedora and other newer Red Hat distributions. <3> Use `zypper` on OpenSUSE based distributions -[NOTE] -================================================== - -The configured repository is disabled by default. This eliminates the possibility of accidentally +NOTE: The configured repository is disabled by default. This eliminates the possibility of accidentally upgrading `elasticsearch` when upgrading the rest of the system. Each install or upgrade command must explicitly enable the repository as indicated in the sample commands above. From beda30fdd28cfd857f9f97845eddb98f8993467a Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Sun, 8 Dec 2019 14:14:16 +0100 Subject: [PATCH 109/686] Cleanup some in o.e.transport (#49901) Cleaning up some obvious compile warnings and dead code. --- .../transport/ConnectionProfile.java | 8 ------- .../transport/InboundHandler.java | 22 +++++++++---------- .../transport/RemoteClusterService.java | 2 -- .../elasticsearch/transport/TcpTransport.java | 15 ------------- .../transport/TransportRequestOptions.java | 4 ---- 5 files changed, 11 insertions(+), 40 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/ConnectionProfile.java b/server/src/main/java/org/elasticsearch/transport/ConnectionProfile.java index 66db091557f48..cd392fbfcbdc5 100644 --- a/server/src/main/java/org/elasticsearch/transport/ConnectionProfile.java +++ b/server/src/main/java/org/elasticsearch/transport/ConnectionProfile.java @@ -93,14 +93,6 @@ public static ConnectionProfile buildDefaultConnectionProfile(Settings settings) return builder.build(); } - /** - * Builds a connection profile that is dedicated to a single channel type. Use this - * when opening single use connections - */ - public static ConnectionProfile buildSingleChannelProfile(TransportRequestOptions.Type channelType) { - return buildSingleChannelProfile(channelType, null, null, null, null); - } - /** * Builds a connection profile that is dedicated to a single channel type. Allows passing connection and * handshake timeouts and compression settings. diff --git a/server/src/main/java/org/elasticsearch/transport/InboundHandler.java b/server/src/main/java/org/elasticsearch/transport/InboundHandler.java index cb7a14b56970c..f89ee423daf9f 100644 --- a/server/src/main/java/org/elasticsearch/transport/InboundHandler.java +++ b/server/src/main/java/org/elasticsearch/transport/InboundHandler.java @@ -72,8 +72,9 @@ synchronized void registerRequestHandler(Requ requestHandlers = Maps.copyMapWithAddedEntry(requestHandlers, reg.getAction(), reg); } - final RequestHandlerRegistry getRequestHandler(String action) { - return requestHandlers.get(action); + @SuppressWarnings("unchecked") + final RequestHandlerRegistry getRequestHandler(String action) { + return (RequestHandlerRegistry) requestHandlers.get(action); } final Transport.ResponseHandlers getResponseHandlers() { @@ -148,7 +149,7 @@ private void messageReceived(BytesReference reference, TcpChannel channel) throw } } - private void handleRequest(TcpChannel channel, InboundMessage.Request message, int messageLengthBytes) { + private void handleRequest(TcpChannel channel, InboundMessage.Request message, int messageLengthBytes) { final String action = message.getActionName(); final long requestId = message.getRequestId(); final StreamInput stream = message.getStreamInput(); @@ -159,7 +160,7 @@ private void handleRequest(TcpChannel channel, InboundMessage.Request message, i if (message.isHandshake()) { handshaker.handleHandshake(version, channel, requestId, stream); } else { - final RequestHandlerRegistry reg = getRequestHandler(action); + final RequestHandlerRegistry reg = getRequestHandler(action); if (reg == null) { throw new ActionNotFoundTransportException(action); } @@ -171,7 +172,7 @@ private void handleRequest(TcpChannel channel, InboundMessage.Request message, i } transportChannel = new TcpTransportChannel(outboundHandler, channel, action, requestId, version, circuitBreakerService, messageLengthBytes, message.isCompress()); - final TransportRequest request = reg.newRequest(stream); + final T request = reg.newRequest(stream); request.remoteAddress(new TransportAddress(channel.getRemoteAddress())); // in case we throw an exception, i.e. when the limit is hit, we don't want to verify final int nextByte = stream.read(); @@ -180,7 +181,7 @@ private void handleRequest(TcpChannel channel, InboundMessage.Request message, i throw new IllegalStateException("Message not fully read (request) for requestId [" + requestId + "], action [" + action + "], available [" + stream.available() + "]; resetting"); } - threadPool.executor(reg.getExecutor()).execute(new RequestHandler(reg, request, transportChannel)); + threadPool.executor(reg.getExecutor()).execute(new RequestHandler<>(reg, request, transportChannel)); } } catch (Exception e) { // the circuit breaker tripped @@ -245,18 +246,17 @@ private void handleException(final TransportResponseHandler handler, Throwabl }); } - private static class RequestHandler extends AbstractRunnable { - private final RequestHandlerRegistry reg; - private final TransportRequest request; + private static class RequestHandler extends AbstractRunnable { + private final RequestHandlerRegistry reg; + private final T request; private final TransportChannel transportChannel; - RequestHandler(RequestHandlerRegistry reg, TransportRequest request, TransportChannel transportChannel) { + RequestHandler(RequestHandlerRegistry reg, T request, TransportChannel transportChannel) { this.reg = reg; this.request = request; this.transportChannel = transportChannel; } - @SuppressWarnings({"unchecked"}) @Override protected void doRun() throws Exception { reg.processMessageReceived(request, transportChannel); diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index 2bfe3980ed8d3..f398100049a8f 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -62,8 +62,6 @@ public final class RemoteClusterService extends RemoteClusterAware implements Cl private static final Logger logger = LogManager.getLogger(RemoteClusterService.class); - private static final ActionListener noopListener = ActionListener.wrap((x) -> {}, (x) -> {}); - /** * The initial connect timeout for remote cluster connections */ diff --git a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java index 0d8917f12f263..7b3ba6d2c9acc 100644 --- a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java +++ b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java @@ -29,7 +29,6 @@ import org.elasticsearch.action.support.ThreadedActionListener; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.common.Booleans; import org.elasticsearch.common.Strings; import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.common.bytes.BytesArray; @@ -77,7 +76,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.TreeSet; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -136,19 +134,6 @@ public TcpTransport(Settings settings, Version version, ThreadPool threadPool, P this.pageCacheRecycler = pageCacheRecycler; this.networkService = networkService; String nodeName = Node.NODE_NAME_SETTING.get(settings); - final Settings defaultFeatures = TransportSettings.DEFAULT_FEATURES_SETTING.get(settings); - String[] features; - if (defaultFeatures == null) { - features = new String[0]; - } else { - defaultFeatures.names().forEach(key -> { - if (Booleans.parseBoolean(defaultFeatures.get(key)) == false) { - throw new IllegalArgumentException("feature settings must have default [true] value"); - } - }); - // use a sorted set to present the features in a consistent order - features = new TreeSet<>(defaultFeatures.names()).toArray(new String[defaultFeatures.names().size()]); - } BigArrays bigArrays = new BigArrays(pageCacheRecycler, circuitBreakerService, CircuitBreaker.IN_FLIGHT_REQUESTS); this.outboundHandler = new OutboundHandler(nodeName, version, threadPool, bigArrays); diff --git a/server/src/main/java/org/elasticsearch/transport/TransportRequestOptions.java b/server/src/main/java/org/elasticsearch/transport/TransportRequestOptions.java index 7ea992f547f6e..59d5a59498a62 100644 --- a/server/src/main/java/org/elasticsearch/transport/TransportRequestOptions.java +++ b/server/src/main/java/org/elasticsearch/transport/TransportRequestOptions.java @@ -53,10 +53,6 @@ public static Builder builder() { return new Builder(); } - public static Builder builder(TransportRequestOptions options) { - return new Builder().withTimeout(options.timeout).withType(options.type()); - } - public static class Builder { private TimeValue timeout; private Type type = Type.REG; From 2c54f94f3558f2f9e9bb23da289575ab981d2b97 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Sun, 8 Dec 2019 14:52:32 -0800 Subject: [PATCH 110/686] Adding best compression inital commit --- .../xpack/core/ilm/CloseIndexStep.java | 42 +++++ .../xpack/core/ilm/ForceMergeAction.java | 42 ++++- .../xpack/core/ilm/OpenIndexStep.java | 38 +++++ .../xpack/core/ilm/WaitForIndexGreenStep.java | 98 +++++++++++ .../xpack/core/ilm/CloseIndexStepTest.java | 155 +++++++++++++++++ .../xpack/core/ilm/ForceMergeActionTests.java | 77 +++++++-- .../xpack/core/ilm/OpenIndexStepTest.java | 158 ++++++++++++++++++ .../ilm/TimeseriesLifecycleTypeTests.java | 4 +- .../core/ilm/WaitForIndexGreenStepTest.java | 155 +++++++++++++++++ 9 files changed, 745 insertions(+), 24 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java new file mode 100644 index 0000000000000..8f78ce75653e5 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.close.CloseIndexRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateObserver; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.xpack.core.ilm.AsyncActionStep; + +/** + * Invokes a Close Index Step on a index. + */ +public class CloseIndexStep extends AsyncActionStep { + public static final String NAME = "close-index"; + + CloseIndexStep(StepKey key, StepKey nextStepKey, Client client) { + super(key, nextStepKey, client); + } + + @Override + public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, ClusterStateObserver observer, Listener listener) { + if(indexMetaData.getState() == IndexMetaData.State.OPEN) { + CloseIndexRequest request = new CloseIndexRequest(indexMetaData.getIndex().getName()); + getClient().admin().indices() + .close(request, ActionListener.wrap(closeIndexResponse -> listener.onResponse(true), listener::onFailure)); + } + else { + listener.onResponse(true); + } + } + + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index eb5f2b61017a1..5d4227a766f58 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -15,6 +15,8 @@ import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.codec.CodecService; +import org.elasticsearch.index.engine.EngineConfig; import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; @@ -28,42 +30,53 @@ public class ForceMergeAction implements LifecycleAction { public static final String NAME = "forcemerge"; public static final ParseField MAX_NUM_SEGMENTS_FIELD = new ParseField("max_num_segments"); + public static final ParseField BEST_COMPRESSION_FIELD = new ParseField("best_compression"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, a -> { int maxNumSegments = (int) a[0]; - return new ForceMergeAction(maxNumSegments); + boolean bestCompression = a[1] != null && (boolean) a[1]; + return new ForceMergeAction(maxNumSegments, bestCompression); }); static { PARSER.declareInt(ConstructingObjectParser.constructorArg(), MAX_NUM_SEGMENTS_FIELD); + PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), BEST_COMPRESSION_FIELD); } private final int maxNumSegments; + private final boolean bestCompression; public static ForceMergeAction parse(XContentParser parser) { return PARSER.apply(parser, null); } - public ForceMergeAction(int maxNumSegments) { + public ForceMergeAction(int maxNumSegments, boolean bestCompression) { if (maxNumSegments <= 0) { throw new IllegalArgumentException("[" + MAX_NUM_SEGMENTS_FIELD.getPreferredName() + "] must be a positive integer"); } this.maxNumSegments = maxNumSegments; + this.bestCompression = bestCompression; } public ForceMergeAction(StreamInput in) throws IOException { this.maxNumSegments = in.readVInt(); + this.bestCompression = in.readBoolean(); } public int getMaxNumSegments() { return maxNumSegments; } + public boolean isBestCompression() { + return bestCompression; + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeVInt(maxNumSegments); + out.writeBoolean(bestCompression); } @Override @@ -80,6 +93,7 @@ public boolean isSafeAction() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(MAX_NUM_SEGMENTS_FIELD.getPreferredName(), maxNumSegments); + builder.field(BEST_COMPRESSION_FIELD.getPreferredName(), bestCompression); builder.endObject(); return builder; } @@ -92,6 +106,25 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) StepKey forceMergeKey = new StepKey(phase, NAME, ForceMergeStep.NAME); StepKey countKey = new StepKey(phase, NAME, SegmentCountStep.NAME); + + if (this.bestCompression) { + StepKey closeKey = new StepKey(phase, NAME, CloseIndexStep.NAME); + StepKey openKey = new StepKey(phase, NAME, OpenIndexStep.NAME); + StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexGreenStep.NAME); + StepKey updateCompressionKey = new StepKey(phase, NAME, UpdateSettingsStep.NAME); + Settings bestCompressionSettings = Settings.builder() + .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), CodecService.BEST_COMPRESSION_CODEC).build(); + + CloseIndexStep closeIndexStep = new CloseIndexStep(closeKey, updateCompressionKey, client); + UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey, + openKey, client, bestCompressionSettings); + OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client); + WaitForIndexGreenStep waitForIndexGreenStep = new WaitForIndexGreenStep(waitForGreenIndexKey, forceMergeKey); + ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, nextStepKey, client, maxNumSegments); + return Arrays.asList(closeIndexStep, updateBestCompressionSettings, + openIndexStep, waitForIndexGreenStep, forceMergeStep); + } + UpdateSettingsStep readOnlyStep = new UpdateSettingsStep(readOnlyKey, forceMergeKey, client, readOnlySettings); ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, countKey, client, maxNumSegments); SegmentCountStep segmentCountStep = new SegmentCountStep(countKey, nextStepKey, client, maxNumSegments); @@ -100,7 +133,7 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) @Override public int hashCode() { - return Objects.hash(maxNumSegments); + return Objects.hash(maxNumSegments, bestCompression); } @Override @@ -112,7 +145,8 @@ public boolean equals(Object obj) { return false; } ForceMergeAction other = (ForceMergeAction) obj; - return Objects.equals(maxNumSegments, other.maxNumSegments); + return Objects.equals(maxNumSegments, other.maxNumSegments) + && Objects.equals(bestCompression, other.bestCompression); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java new file mode 100644 index 0000000000000..793db17eb8376 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.open.OpenIndexRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateObserver; +import org.elasticsearch.cluster.metadata.IndexMetaData; + +final class OpenIndexStep extends AsyncActionStep { + + static final String NAME = "open-index"; + + OpenIndexStep(StepKey key, StepKey nextStepKey, Client client) { + super(key, nextStepKey, client); + } + + @Override + public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, + ClusterStateObserver observer, Listener listener) { + if (indexMetaData.getState() == IndexMetaData.State.CLOSE) { + OpenIndexRequest request = new OpenIndexRequest(indexMetaData.getIndex().getName()); + getClient().admin().indices() + .open(request, + ActionListener.wrap(closeIndexResponse -> listener.onResponse(true), listener::onFailure)); + + } else { + listener.onResponse(true); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java new file mode 100644 index 0000000000000..f001644da2022 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ +package org.elasticsearch.xpack.core.ilm; + +import com.carrotsearch.hppc.cursors.IntObjectCursor; +import com.carrotsearch.hppc.cursors.ObjectCursor; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.routing.IndexRoutingTable; +import org.elasticsearch.cluster.routing.IndexShardRoutingTable; +import org.elasticsearch.cluster.routing.RoutingTable; +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.Index; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +class WaitForIndexGreenStep extends ClusterStateWaitStep { + + static final String NAME = "wait-for-index-green-step"; + + WaitForIndexGreenStep(StepKey key, StepKey nextStepKey) { + super(key, nextStepKey); + } + + @Override + public Result isConditionMet(Index index, ClusterState clusterState) { + RoutingTable routingTable = clusterState.routingTable(); + IndexRoutingTable indexRoutingTable = routingTable.index(index); + if (indexRoutingTable == null) { + return new Result(false, new Info("index is red; no IndexRoutingTable")); + } + + boolean indexIsGreen = false; + if(indexRoutingTable.allPrimaryShardsActive()) { + boolean replicaIndexIsGreen = false; + for (ObjectCursor shardRouting : indexRoutingTable.getShards().values()) { + replicaIndexIsGreen = shardRouting.value.replicaShards().stream().allMatch(ShardRouting::active); + if(!replicaIndexIsGreen) { + return new Result(false, new Info("index is yellow; not all replica shards are active")); + } + } + indexIsGreen = replicaIndexIsGreen; + } + + + if (indexIsGreen) { + return new Result(true, null); + } else { + return new Result(false, new Info("index is not green; not all shards are active")); + } + } + + static final class Info implements ToXContentObject { + + static final ParseField MESSAGE_FIELD = new ParseField("message"); + + private final String message; + + Info(String message) { + this.message = message; + } + + String getMessage() { + return message; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(MESSAGE_FIELD.getPreferredName(), message); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Info info = (Info) o; + return Objects.equals(getMessage(), info.getMessage()); + } + + @Override + public int hashCode() { + return Objects.hash(getMessage()); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java new file mode 100644 index 0000000000000..7d7a64edc6336 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.close.CloseIndexRequest; +import org.elasticsearch.action.admin.indices.close.CloseIndexResponse; +import org.elasticsearch.client.AdminClient; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.IndicesAdminClient; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.junit.Before; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import java.util.Collections; + +import static org.hamcrest.Matchers.equalTo; + +/** + * Created by sivagurunathanvelayutham on Dec, 2019 + */ +public class CloseIndexStepTest extends AbstractStepTestCase { + + private Client client; + + @Before + public void setup() { + client = Mockito.mock(Client.class); + } + + @Override + protected CloseIndexStep createRandomInstance() { + return new CloseIndexStep(randomStepKey(), randomStepKey(), client); + } + + @Override + protected CloseIndexStep mutateInstance(CloseIndexStep instance) { + Step.StepKey key = instance.getKey(); + Step.StepKey nextKey = instance.getNextStepKey(); + + switch (between(0, 1)) { + case 0: + key = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 1: + nextKey = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + + return new CloseIndexStep(key, nextKey, client); + } + + @Override + protected CloseIndexStep copyInstance(CloseIndexStep instance) { + return new CloseIndexStep(instance.getKey(), instance.getNextStepKey(), instance.getClient()); + } + + public void testPerformAction() { + IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); + + CloseIndexStep step = createRandomInstance(); + + AdminClient adminClient = Mockito.mock(AdminClient.class); + IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class); + + Mockito.when(client.admin()).thenReturn(adminClient); + Mockito.when(adminClient.indices()).thenReturn(indicesClient); + + Mockito.doAnswer((Answer) invocation -> { + CloseIndexRequest request = (CloseIndexRequest) invocation.getArguments()[0]; + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocation.getArguments()[1]; + assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()})); + listener.onResponse(new CloseIndexResponse(true, true, + Collections.singletonList(new CloseIndexResponse.IndexResult(indexMetaData.getIndex())))); + return null; + }).when(indicesClient).close(Mockito.any(), Mockito.any()); + + SetOnce actionCompleted = new SetOnce<>(); + + step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() { + + @Override + public void onResponse(boolean complete) { + actionCompleted.set(complete); + } + + @Override + public void onFailure(Exception e) { + throw new AssertionError("Unexpected method call", e); + } + }); + + assertEquals(true, actionCompleted.get()); + Mockito.verify(client, Mockito.only()).admin(); + Mockito.verify(adminClient, Mockito.only()).indices(); + Mockito.verify(indicesClient, Mockito.only()).close(Mockito.any(), Mockito.any()); + } + + + public void testPerformActionFailure() { + IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); + + CloseIndexStep step = createRandomInstance(); + Exception exception = new RuntimeException(); + AdminClient adminClient = Mockito.mock(AdminClient.class); + IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class); + + Mockito.when(client.admin()).thenReturn(adminClient); + Mockito.when(adminClient.indices()).thenReturn(indicesClient); + + Mockito.doAnswer((Answer) invocation -> { + CloseIndexRequest request = (CloseIndexRequest) invocation.getArguments()[0]; + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocation.getArguments()[1]; + assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()})); + listener.onFailure(exception); + return null; + }).when(indicesClient).close(Mockito.any(), Mockito.any()); + + SetOnce exceptionThrown = new SetOnce<>(); + + step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() { + + @Override + public void onResponse(boolean complete) { + throw new AssertionError("Unexpected method call"); + } + + @Override + public void onFailure(Exception e) { + assertSame(exception, e); + exceptionThrown.set(true); + } + }); + + assertEquals(true, exceptionThrown.get()); + Mockito.verify(client, Mockito.only()).admin(); + Mockito.verify(adminClient, Mockito.only()).indices(); + Mockito.verify(indicesClient, Mockito.only()).close(Mockito.any(), Mockito.any()); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index 92fe40a08dfcc..c12f6e7459bd2 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -13,6 +13,8 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.index.codec.CodecService; +import org.elasticsearch.index.engine.EngineConfig; import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; @@ -33,14 +35,20 @@ protected ForceMergeAction createTestInstance() { } static ForceMergeAction randomInstance() { - return new ForceMergeAction(randomIntBetween(1, 100)); + return new ForceMergeAction(randomIntBetween(1, 100), randomBoolean()); } @Override protected ForceMergeAction mutateInstance(ForceMergeAction instance) { int maxNumSegments = instance.getMaxNumSegments(); - maxNumSegments = maxNumSegments + randomIntBetween(1, 10); - return new ForceMergeAction(maxNumSegments); + boolean bestCompression = instance.isBestCompression(); + if(randomBoolean()) { + maxNumSegments = maxNumSegments + randomIntBetween(1, 10); + } + else { + bestCompression = !bestCompression; + } + return new ForceMergeAction(maxNumSegments, bestCompression); } @Override @@ -48,21 +56,7 @@ protected Reader instanceReader() { return ForceMergeAction::new; } - public void testMissingMaxNumSegments() throws IOException { - BytesReference emptyObject = BytesReference.bytes(JsonXContent.contentBuilder().startObject().endObject()); - XContentParser parser = XContentHelper.createParser(null, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, - emptyObject, XContentType.JSON); - Exception e = expectThrows(IllegalArgumentException.class, () -> ForceMergeAction.parse(parser)); - assertThat(e.getMessage(), equalTo("Required [max_num_segments]")); - } - - public void testInvalidNegativeSegmentNumber() { - Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0))); - assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer")); - } - - public void testToSteps() { - ForceMergeAction instance = createTestInstance(); + private void assertNonBestCompression(ForceMergeAction instance) { String phase = randomAlphaOfLength(5); StepKey nextStepKey = new StepKey(randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10)); List steps = instance.toSteps(null, phase, nextStepKey); @@ -79,4 +73,51 @@ public void testToSteps() { assertThat(thirdStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, SegmentCountStep.NAME))); assertThat(thirdStep.getNextStepKey(), equalTo(nextStepKey)); } + + private void assertBestCompression(ForceMergeAction instance) { + String phase = randomAlphaOfLength(5); + StepKey nextStepKey = new StepKey(randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10)); + List steps = instance.toSteps(null, phase, nextStepKey); + assertNotNull(steps); + assertEquals(5, steps.size()); + CloseIndexStep firstStep = (CloseIndexStep) steps.get(0); + UpdateSettingsStep secondStep = (UpdateSettingsStep) steps.get(1); + OpenIndexStep thirdStep = (OpenIndexStep) steps.get(2); + WaitForIndexGreenStep fourthStep = (WaitForIndexGreenStep) steps.get(3); + ForceMergeStep fifthStep = (ForceMergeStep) steps.get(4); + assertThat(firstStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, CloseIndexStep.NAME))); + assertThat(firstStep.getNextStepKey(), equalTo(secondStep.getKey())); + assertThat(secondStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, UpdateSettingsStep.NAME))); + assertThat(secondStep.getSettings().get(EngineConfig.INDEX_CODEC_SETTING.getKey()), equalTo(CodecService.BEST_COMPRESSION_CODEC)); + assertThat(secondStep.getNextStepKey(), equalTo(thirdStep.getKey())); + assertThat(thirdStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, OpenIndexStep.NAME))); + assertThat(thirdStep.getNextStepKey(), equalTo(fourthStep)); + assertThat(fourthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, WaitForIndexGreenStep.NAME))); + assertThat(fourthStep.getNextStepKey(), equalTo(fifthStep)); + assertThat(fifthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, ForceMergeStep.NAME))); + assertThat(fifthStep.getNextStepKey(), equalTo(nextStepKey)); + } + + public void testMissingMaxNumSegments() throws IOException { + BytesReference emptyObject = BytesReference.bytes(JsonXContent.contentBuilder().startObject().endObject()); + XContentParser parser = XContentHelper.createParser(null, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, + emptyObject, XContentType.JSON); + Exception e = expectThrows(IllegalArgumentException.class, () -> ForceMergeAction.parse(parser)); + assertThat(e.getMessage(), equalTo("Required [max_num_segments, best_compression]")); + } + + public void testInvalidNegativeSegmentNumber() { + Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0), false)); + assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer")); + } + + public void testToSteps() { + ForceMergeAction instance = createTestInstance(); + if (instance.isBestCompression()) { + assertBestCompression(instance); + } + else { + assertNonBestCompression(instance); + } + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java new file mode 100644 index 0000000000000..164d5f7fe4c3c --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.open.OpenIndexRequest; +import org.elasticsearch.action.admin.indices.open.OpenIndexResponse; +import org.elasticsearch.client.AdminClient; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.IndicesAdminClient; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.junit.Before; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import static org.hamcrest.Matchers.equalTo; + +/** + * Created by sivagurunathanvelayutham on Dec, 2019 + */ +public class OpenIndexStepTest extends AbstractStepTestCase { + + private Client client; + + @Before + public void setup() { + client = Mockito.mock(Client.class); + } + + @Override + protected OpenIndexStep createRandomInstance() { + return new OpenIndexStep(randomStepKey(), randomStepKey(), client); + } + + @Override + protected OpenIndexStep mutateInstance(OpenIndexStep instance) { + Step.StepKey key = instance.getKey(); + Step.StepKey nextKey = instance.getNextStepKey(); + + switch (between(0, 1)) { + case 0: + key = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 1: + nextKey = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + + return new OpenIndexStep(key, nextKey, client); + } + + @Override + protected OpenIndexStep copyInstance(OpenIndexStep instance) { + return new OpenIndexStep(instance.getKey(), instance.getNextStepKey(), instance.getClient()); + } + + public void testPerformAction() { + IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1, 5)) + .numberOfReplicas(randomIntBetween(0, 5)) + .state(IndexMetaData.State.CLOSE) + .build(); + + OpenIndexStep step = createRandomInstance(); + + AdminClient adminClient = Mockito.mock(AdminClient.class); + IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class); + + Mockito.when(client.admin()).thenReturn(adminClient); + Mockito.when(adminClient.indices()).thenReturn(indicesClient); + + Mockito.doAnswer((Answer) invocation -> { + OpenIndexRequest request = (OpenIndexRequest) invocation.getArguments()[0]; + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocation.getArguments()[1]; + assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()})); + listener.onResponse(new OpenIndexResponse(true, true)); + return null; + }).when(indicesClient).open(Mockito.any(), Mockito.any()); + + SetOnce actionCompleted = new SetOnce<>(); + + step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() { + + @Override + public void onResponse(boolean complete) { + actionCompleted.set(complete); + } + + @Override + public void onFailure(Exception e) { + throw new AssertionError("Unexpected method call", e); + } + }); + + assertEquals(true, actionCompleted.get()); + Mockito.verify(client, Mockito.only()).admin(); + Mockito.verify(adminClient, Mockito.only()).indices(); + Mockito.verify(indicesClient, Mockito.only()).open(Mockito.any(), Mockito.any()); + } + + + public void testPerformActionFailure() { + IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1, 5)) + .numberOfReplicas(randomIntBetween(0, 5)) + .state(IndexMetaData.State.CLOSE) + .build(); + + OpenIndexStep step = createRandomInstance(); + Exception exception = new RuntimeException(); + AdminClient adminClient = Mockito.mock(AdminClient.class); + IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class); + + Mockito.when(client.admin()).thenReturn(adminClient); + Mockito.when(adminClient.indices()).thenReturn(indicesClient); + + Mockito.doAnswer((Answer) invocation -> { + OpenIndexRequest request = (OpenIndexRequest) invocation.getArguments()[0]; + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocation.getArguments()[1]; + assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()})); + listener.onFailure(exception); + return null; + }).when(indicesClient).open(Mockito.any(), Mockito.any()); + + SetOnce exceptionThrown = new SetOnce<>(); + + step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() { + + @Override + public void onResponse(boolean complete) { + throw new AssertionError("Unexpected method call"); + } + + @Override + public void onFailure(Exception e) { + assertSame(exception, e); + exceptionThrown.set(true); + } + }); + + assertEquals(true, exceptionThrown.get()); + Mockito.verify(client, Mockito.only()).admin(); + Mockito.verify(adminClient, Mockito.only()).indices(); + Mockito.verify(indicesClient, Mockito.only()).open(Mockito.any(), Mockito.any()); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java index a76a22fcc7821..49bc71c55971f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java @@ -34,7 +34,7 @@ public class TimeseriesLifecycleTypeTests extends ESTestCase { private static final AllocateAction TEST_ALLOCATE_ACTION = new AllocateAction(2, Collections.singletonMap("node", "node1"),null, null); private static final DeleteAction TEST_DELETE_ACTION = new DeleteAction(); - private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1); + private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1, false); private static final RolloverAction TEST_ROLLOVER_ACTION = new RolloverAction(new ByteSizeValue(1), null, null); private static final ShrinkAction TEST_SHRINK_ACTION = new ShrinkAction(1); private static final ReadOnlyAction TEST_READ_ONLY_ACTION = new ReadOnlyAction(); @@ -492,7 +492,7 @@ private ConcurrentMap convertActionNamesToActions(Strin case DeleteAction.NAME: return new DeleteAction(); case ForceMergeAction.NAME: - return new ForceMergeAction(1); + return new ForceMergeAction(1, false); case ReadOnlyAction.NAME: return new ReadOnlyAction(); case RolloverAction.NAME: diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java new file mode 100644 index 0000000000000..ec4f73c2ec592 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.routing.IndexRoutingTable; +import org.elasticsearch.cluster.routing.RoutingTable; +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.cluster.routing.ShardRoutingState; +import org.elasticsearch.cluster.routing.TestShardRouting; +import org.elasticsearch.xpack.core.ilm.Step.StepKey; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.IsNull.notNullValue; + +public class WaitForIndexGreenStepTest extends AbstractStepTestCase { + + @Override + protected WaitForIndexGreenStep createRandomInstance() { + StepKey stepKey = randomStepKey(); + StepKey nextStepKey = randomStepKey(); + return new WaitForIndexGreenStep(stepKey, nextStepKey); + } + + @Override + protected WaitForIndexGreenStep mutateInstance(WaitForIndexGreenStep instance) { + StepKey key = instance.getKey(); + StepKey nextKey = instance.getNextStepKey(); + + if (randomBoolean()) { + key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + } else { + nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + } + + return new WaitForIndexGreenStep(key, nextKey); + } + + @Override + protected WaitForIndexGreenStep copyInstance(WaitForIndexGreenStep instance) { + return new WaitForIndexGreenStep(instance.getKey(), instance.getNextStepKey()); + } + + public void testConditionMet() { + IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(2) + .build(); + + ShardRouting shardRouting = + TestShardRouting.newShardRouting("test_index", 0, "1", true, ShardRoutingState.STARTED); + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex()) + .addShard(shardRouting).build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) + .build(); + + WaitForIndexGreenStep step = createRandomInstance(); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(true)); + assertThat(result.getInfomationContext(), nullValue()); + } + + public void testConditionNotMet() { + IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + + ShardRouting shardRouting = + TestShardRouting.newShardRouting("test_index", 0, "1", true, ShardRoutingState.INITIALIZING); + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex()) + .addShard(shardRouting).build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) + .build(); + + WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(false)); + WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + assertThat(info, notNullValue()); + assertThat(info.getMessage(), equalTo("index is not green; not all shards are active")); + } + + public void testConditionNotMetWithYellow() { + IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(2) + .build(); + + ShardRouting shardRouting = + TestShardRouting.newShardRouting("test_index", 0, "1", true, ShardRoutingState.STARTED); + + ShardRouting replicaShardRouting = + TestShardRouting.newShardRouting("test_index", 0, "2", false, ShardRoutingState.INITIALIZING); + + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex()) + .addShard(shardRouting) + .addShard(replicaShardRouting) + .build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) + .build(); + + WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(false)); + WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + assertThat(info, notNullValue()); + assertThat(info.getMessage(), equalTo("index is yellow; not all replica shards are active")); + } + + public void testConditionNotMetNoIndexRoutingTable() { + IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().build()) + .build(); + + WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(false)); + WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + assertThat(info, notNullValue()); + assertThat(info.getMessage(), equalTo("index is red; no IndexRoutingTable")); + } +} + From b7c35b04cfde15e5ad7af718a8633687e6e3d7a4 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Sun, 8 Dec 2019 16:27:19 -0800 Subject: [PATCH 111/686] Checkstyle and License --- .../xpack/core/ilm/CloseIndexStep.java | 24 ++++++++++++++----- .../xpack/core/ilm/OpenIndexStep.java | 21 ++++++++++++---- .../xpack/core/ilm/WaitForIndexGreenStep.java | 24 ++++++++++++------- .../xpack/core/ilm/CloseIndexStepTest.java | 21 ++++++++++++---- .../xpack/core/ilm/OpenIndexStepTest.java | 21 ++++++++++++---- .../core/ilm/WaitForIndexGreenStepTest.java | 21 ++++++++++++---- 6 files changed, 98 insertions(+), 34 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java index 8f78ce75653e5..1bac9ea91813b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -1,9 +1,20 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; @@ -27,7 +38,8 @@ public class CloseIndexStep extends AsyncActionStep { } @Override - public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, ClusterStateObserver observer, Listener listener) { + public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, + ClusterStateObserver observer, Listener listener) { if(indexMetaData.getState() == IndexMetaData.State.OPEN) { CloseIndexRequest request = new CloseIndexRequest(indexMetaData.getIndex().getName()); getClient().admin().indices() diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java index 793db17eb8376..fc9cf75284411 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java @@ -1,9 +1,20 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java index f001644da2022..fc602a1a250f7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java @@ -1,13 +1,23 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; -import com.carrotsearch.hppc.cursors.IntObjectCursor; import com.carrotsearch.hppc.cursors.ObjectCursor; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.routing.IndexRoutingTable; @@ -20,9 +30,7 @@ import org.elasticsearch.index.Index; import java.io.IOException; -import java.util.List; import java.util.Objects; -import java.util.stream.Stream; class WaitForIndexGreenStep extends ClusterStateWaitStep { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java index 7d7a64edc6336..409180e2118d2 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java @@ -1,9 +1,20 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java index 164d5f7fe4c3c..95d7da6457ce8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java @@ -1,9 +1,20 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java index ec4f73c2ec592..87c1f19a56e4f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java @@ -1,9 +1,20 @@ /* - * Copyright (c) 2019. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. - * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. - * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. - * Vestibulum commodo. Ut rhoncus gravida arcu. + * 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. */ package org.elasticsearch.xpack.core.ilm; From c8e054393bf5e5d0672367a986216d9ba60dcc99 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Sun, 8 Dec 2019 16:36:06 -0800 Subject: [PATCH 112/686] Removing Comments from file --- .../java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java | 3 --- .../org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java | 3 --- .../org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java | 3 --- 3 files changed, 9 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java index 1bac9ea91813b..53f7e469c3bbe 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -27,9 +27,6 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.xpack.core.ilm.AsyncActionStep; -/** - * Invokes a Close Index Step on a index. - */ public class CloseIndexStep extends AsyncActionStep { public static final String NAME = "close-index"; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java index 409180e2118d2..53b2c47d00b69 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java @@ -36,9 +36,6 @@ import static org.hamcrest.Matchers.equalTo; -/** - * Created by sivagurunathanvelayutham on Dec, 2019 - */ public class CloseIndexStepTest extends AbstractStepTestCase { private Client client; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java index 95d7da6457ce8..558e4ac824a11 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java @@ -34,9 +34,6 @@ import static org.hamcrest.Matchers.equalTo; -/** - * Created by sivagurunathanvelayutham on Dec, 2019 - */ public class OpenIndexStepTest extends AbstractStepTestCase { private Client client; From 67884b31e2d2799afbed7e052848c1ee78ba8def Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Mon, 9 Dec 2019 08:27:41 +0100 Subject: [PATCH 113/686] Randomly run CCR tests with _source disabled (#49922) Makes sure that CCR also properly works with _source disabled. Changes one exception in LuceneChangesSnapshot as the case of missing _recovery_source because of a missing lease was not properly properly bubbled up to CCR (testIndexFallBehind was failing). --- .../index/engine/LuceneChangesSnapshot.java | 2 +- .../org/elasticsearch/xpack/CcrIntegTestCase.java | 13 +++++++++++++ .../elasticsearch/xpack/ccr/CcrRepositoryIT.java | 6 ++++-- .../xpack/ccr/CcrRetentionLeaseIT.java | 6 ++++-- .../elasticsearch/xpack/ccr/IndexFollowingIT.java | 6 ++++-- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java b/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java index 78b6587eeac0f..7ae37965a2ffd 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java +++ b/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java @@ -255,7 +255,7 @@ private Translog.Operation readDocAsOp(int docIndex) throws IOException { // TODO: Callers should ask for the range that source should be retained. Thus we should always // check for the existence source once we make peer-recovery to send ops after the local checkpoint. if (requiredFullRange) { - throw new IllegalStateException("source not found for seqno=" + seqNo + + throw new MissingHistoryOperationsException("source not found for seqno=" + seqNo + " from_seqno=" + fromSeqNo + " to_seqno=" + toSeqNo); } else { skippedOperations++; diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java index cc48c080ac8ce..fbd537ed18d98 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java @@ -405,6 +405,14 @@ protected void ensureNoCcrTasks() throws Exception { }, 30, TimeUnit.SECONDS); } + + @Before + public void setupSourceEnabledOrDisabled() { + sourceEnabled = randomBoolean(); + } + + protected boolean sourceEnabled; + protected String getIndexSettings(final int numberOfShards, final int numberOfReplicas, final Map additionalIndexSettings) throws IOException { final String settings; @@ -435,6 +443,11 @@ protected String getIndexSettings(final int numberOfShards, final int numberOfRe builder.endObject(); } builder.endObject(); + if (sourceEnabled == false) { + builder.startObject("_source"); + builder.field("enabled", false); + builder.endObject(); + } } builder.endObject(); } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrRepositoryIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrRepositoryIT.java index 1bedab2134adc..a8f393073a745 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrRepositoryIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrRepositoryIT.java @@ -446,8 +446,10 @@ public void testFollowerMappingIsUpdated() throws IOException { private void assertExpectedDocument(String followerIndex, final int value) { final GetResponse getResponse = followerClient().prepareGet(followerIndex, Integer.toString(value)).get(); assertTrue("Doc with id [" + value + "] is missing", getResponse.isExists()); - assertTrue((getResponse.getSource().containsKey("f"))); - assertThat(getResponse.getSource().get("f"), equalTo(value)); + if (sourceEnabled) { + assertTrue((getResponse.getSource().containsKey("f"))); + assertThat(getResponse.getSource().get("f"), equalTo(value)); + } } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrRetentionLeaseIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrRetentionLeaseIT.java index b7ebb4dddad92..b74ca1dbc985a 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrRetentionLeaseIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrRetentionLeaseIT.java @@ -1075,8 +1075,10 @@ private String getRetentionLeaseId(String followerIndex, String followerUUID, St private void assertExpectedDocument(final String followerIndex, final int value) { final GetResponse getResponse = followerClient().prepareGet(followerIndex, Integer.toString(value)).get(); assertTrue("doc with id [" + value + "] is missing", getResponse.isExists()); - assertTrue((getResponse.getSource().containsKey("f"))); - assertThat(getResponse.getSource().get("f"), equalTo(value)); + if (sourceEnabled) { + assertTrue((getResponse.getSource().containsKey("f"))); + assertThat(getResponse.getSource().get("f"), equalTo(value)); + } } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IndexFollowingIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IndexFollowingIT.java index b23dbbc68c315..7bede18aea08d 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IndexFollowingIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IndexFollowingIT.java @@ -1519,8 +1519,10 @@ private CheckedRunnable assertExpectedDocumentRunnable(final int key, return () -> { final GetResponse getResponse = followerClient().prepareGet("index2", Integer.toString(key)).get(); assertTrue("Doc with id [" + key + "] is missing", getResponse.isExists()); - assertTrue((getResponse.getSource().containsKey("f"))); - assertThat(getResponse.getSource().get("f"), equalTo(value)); + if (sourceEnabled) { + assertTrue((getResponse.getSource().containsKey("f"))); + assertThat(getResponse.getSource().get("f"), equalTo(value)); + } }; } From f80aa2e47b3ba766ff69ed2d7fd701a440c81b3d Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Mon, 9 Dec 2019 08:28:23 +0100 Subject: [PATCH 114/686] Properly fake corrupted translog (#49918) The fake translog corruption in the test sometimes generates invalid translog files where some assertions do not hold (e.g. minSeqNo <= maxSeqNo or minTranslogGen <= translogGen) Closes #49909 --- .../index/translog/TestTranslog.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/translog/TestTranslog.java b/server/src/test/java/org/elasticsearch/index/translog/TestTranslog.java index f80d0db880057..d78a731674258 100644 --- a/server/src/test/java/org/elasticsearch/index/translog/TestTranslog.java +++ b/server/src/test/java/org/elasticsearch/index/translog/TestTranslog.java @@ -92,11 +92,21 @@ static void corruptRandomTranslogFile(Logger logger, Random random, Path translo // if we crashed while rolling a generation then we might have copied `translog.ckp` to its numbered generation file but // have not yet written a new `translog.ckp`. During recovery we must also verify that this file is intact, so it's ok to // corrupt this file too (either by writing the wrong information, correctly formatted, or by properly corrupting it) - final Checkpoint checkpointCopy = LuceneTestCase.usually(random) ? checkpoint - : new Checkpoint(checkpoint.offset + random.nextInt(2), checkpoint.numOps + random.nextInt(2), - checkpoint.generation + random.nextInt(2), checkpoint.minSeqNo + random.nextInt(2), - checkpoint.maxSeqNo + random.nextInt(2), checkpoint.globalCheckpoint + random.nextInt(2), - checkpoint.minTranslogGeneration + random.nextInt(2), checkpoint.trimmedAboveSeqNo + random.nextInt(2)); + final Checkpoint checkpointCopy; + if (LuceneTestCase.usually(random)) { + checkpointCopy = checkpoint; + } else { + long newTranslogGeneration = checkpoint.generation + random.nextInt(2); + long newMinTranslogGeneration = Math.min(newTranslogGeneration, checkpoint.minTranslogGeneration + random.nextInt(2)); + long newMaxSeqNo = checkpoint.maxSeqNo + random.nextInt(2); + long newMinSeqNo = Math.min(newMaxSeqNo, checkpoint.minSeqNo + random.nextInt(2)); + long newTrimmedAboveSeqNo = Math.min(newMaxSeqNo, checkpoint.trimmedAboveSeqNo + random.nextInt(2)); + + checkpointCopy = new Checkpoint(checkpoint.offset + random.nextInt(2), checkpoint.numOps + random.nextInt(2), + newTranslogGeneration, newMinSeqNo, + newMaxSeqNo, checkpoint.globalCheckpoint + random.nextInt(2), + newMinTranslogGeneration, newTrimmedAboveSeqNo); + } Checkpoint.write(FileChannel::open, unnecessaryCheckpointCopyPath, checkpointCopy, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); From 835a1616824ab8db969abddd6168cdc8a5705642 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 9 Dec 2019 09:02:38 +0100 Subject: [PATCH 115/686] Disable BwC Tests for #49976 (#49977) Disabling BwC tests so #49976 can be merged. --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index df1641dcc9efe..d67acb4a37dfa 100644 --- a/build.gradle +++ b/build.gradle @@ -205,8 +205,8 @@ task verifyVersions { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = true -final String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */ +boolean bwc_tests_enabled = false +final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/49976" /* place a PR link here when committing bwc changes */ if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") From 3cb037c12d02bf54b05bf448a2a18fb0471c985c Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Mon, 9 Dec 2019 11:02:16 +0200 Subject: [PATCH 116/686] [ML] Update expected mem estimate in explain API integ test (#49924) Work in progress in the c++ side is increasing memory estimates a bit and this test fails. At the time of this commit the mem estimate when there is no source query is a about 2Mb. So I am relaxing the test to assert memory estimate is less than 1Mb instead of 500Kb. --- .../xpack/ml/integration/ExplainDataFrameAnalyticsIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java index 540d9f373b7e4..ba00e49456f5f 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java @@ -60,6 +60,6 @@ public void testSourceQueryIsApplied() throws IOException { ExplainDataFrameAnalyticsAction.Response explainResponse = explainDataFrame(config); - assertThat(explainResponse.getMemoryEstimation().getExpectedMemoryWithoutDisk().getKb(), lessThanOrEqualTo(500L)); + assertThat(explainResponse.getMemoryEstimation().getExpectedMemoryWithoutDisk().getKb(), lessThanOrEqualTo(1024L)); } } From afdc6c82d24b7d978134debab6a6ce40f8cb1ea1 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 9 Dec 2019 11:18:39 +0100 Subject: [PATCH 117/686] Reenable BwC Tests After #49976 (#49978) With #49976 merged we can reenable BwC tests. --- build.gradle | 4 ++-- .../elasticsearch/cluster/metadata/RepositoryMetaData.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index d67acb4a37dfa..df1641dcc9efe 100644 --- a/build.gradle +++ b/build.gradle @@ -205,8 +205,8 @@ task verifyVersions { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = false -final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/49976" /* place a PR link here when committing bwc changes */ +boolean bwc_tests_enabled = true +final String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */ if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java index c210c32a9a529..c57f702805504 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java @@ -32,7 +32,7 @@ */ public class RepositoryMetaData { - public static final Version REPO_GEN_IN_CS_VERSION = Version.V_8_0_0; + public static final Version REPO_GEN_IN_CS_VERSION = Version.V_7_6_0; private final String name; private final String type; From a470b6e5d49d27ad816cb9e57b44d5baa9e773ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Mon, 9 Dec 2019 13:21:58 +0100 Subject: [PATCH 118/686] Pass `prediction_field_type` to C++ analytics process (#49861) --- .../ml/dataframe/analyses/Classification.java | 29 +++++- .../dataframe/analyses/DataFrameAnalysis.java | 3 +- .../dataframe/analyses/OutlierDetection.java | 2 +- .../ml/dataframe/analyses/Regression.java | 2 +- .../analyses/ClassificationTests.java | 37 ++++++++ .../analyses/OutlierDetectionTests.java | 4 +- .../dataframe/analyses/RegressionTests.java | 7 ++ .../ml/dataframe/analyses/TypesTests.java | 21 +++++ .../ClassificationEvaluationIT.java | 86 ++++++++++++++---- .../ml/integration/ClassificationIT.java | 90 +++++++++++-------- .../DataFrameDataExtractorFactory.java | 4 + .../extractor/ExtractedFieldsDetector.java | 14 ++- .../process/AnalyticsProcessConfig.java | 19 +++- .../process/AnalyticsProcessManager.java | 19 +++- .../MemoryUsageEstimationProcessManager.java | 3 +- .../xpack/ml/extractor/ExtractedFields.java | 4 +- .../ExtractedFieldsDetectorTests.java | 86 ++++++++---------- .../process/AnalyticsProcessManagerTests.java | 2 + .../AnalyticsResultProcessorTests.java | 4 +- ...oryUsageEstimationProcessManagerTests.java | 2 + .../ml/extractor/ExtractedFieldsTests.java | 2 +- 21 files changed, 313 insertions(+), 127 deletions(-) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/TypesTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java index b075a7606c87c..b4b258ea161fa 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java @@ -65,6 +65,12 @@ public static Classification fromXContent(XContentParser parser, boolean ignoreU Stream.of(Types.categorical(), Types.discreteNumerical(), Types.bool()) .flatMap(Set::stream) .collect(Collectors.toUnmodifiableSet()); + /** + * Name of the parameter passed down to C++. + * This parameter is used to decide which JSON data type from {string, int, bool} to use when writing the prediction. + */ + private static final String PREDICTION_FIELD_TYPE = "prediction_field_type"; + /** * As long as we only support binary classification it makes sense to always report both classes with their probabilities. * This way the user can see if the prediction was made with confidence they need. @@ -152,7 +158,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } @Override - public Map getParams() { + public Map getParams(Map> extractedFields) { Map params = new HashMap<>(); params.put(DEPENDENT_VARIABLE.getPreferredName(), dependentVariable); params.putAll(boostedTreeParams.getParams()); @@ -160,9 +166,30 @@ public Map getParams() { if (predictionFieldName != null) { params.put(PREDICTION_FIELD_NAME.getPreferredName(), predictionFieldName); } + String predictionFieldType = getPredictionFieldType(extractedFields.get(dependentVariable)); + if (predictionFieldType != null) { + params.put(PREDICTION_FIELD_TYPE, predictionFieldType); + } return params; } + private static String getPredictionFieldType(Set dependentVariableTypes) { + if (dependentVariableTypes == null) { + return null; + } + if (Types.categorical().containsAll(dependentVariableTypes)) { + return "string"; + } + if (Types.bool().containsAll(dependentVariableTypes)) { + return "bool"; + } + if (Types.discreteNumerical().containsAll(dependentVariableTypes)) { + // C++ process uses int64_t type, so it is safe for the dependent variable to use long numbers. + return "int"; + } + return null; + } + @Override public boolean supportsCategoricalFields() { return true; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/DataFrameAnalysis.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/DataFrameAnalysis.java index 0ca32cde4021c..d0af0a452a474 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/DataFrameAnalysis.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/DataFrameAnalysis.java @@ -16,8 +16,9 @@ public interface DataFrameAnalysis extends ToXContentObject, NamedWriteable { /** * @return The analysis parameters as a map + * @param extractedFields map of (name, types) for all the extracted fields */ - Map getParams(); + Map getParams(Map> extractedFields); /** * @return {@code true} if this analysis supports fields with categorical values (i.e. text, keyword, ip) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetection.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetection.java index d4cefe884b53b..70b3cfb9fe246 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetection.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetection.java @@ -192,7 +192,7 @@ public int hashCode() { } @Override - public Map getParams() { + public Map getParams(Map> extractedFields) { Map params = new HashMap<>(); if (nNeighbors != null) { params.put(N_NEIGHBORS.getPreferredName(), nNeighbors); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java index 6fa163dd65ca0..01388f01d807c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java @@ -124,7 +124,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } @Override - public Map getParams() { + public Map getParams(Map> extractedFields) { Map params = new HashMap<>(); params.put(DEPENDENT_VARIABLE.getPreferredName(), dependentVariable); params.putAll(boostedTreeParams.getParams()); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java index 8306d08af7979..61d6b4dfe3f7a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java @@ -8,9 +8,14 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.BooleanFieldMapper; +import org.elasticsearch.index.mapper.KeywordFieldMapper; +import org.elasticsearch.index.mapper.NumberFieldMapper; import org.elasticsearch.test.AbstractSerializingTestCase; import java.io.IOException; +import java.util.Map; +import java.util.Set; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -115,6 +120,38 @@ public void testGetTrainingPercent() { assertThat(classification.getTrainingPercent(), equalTo(100.0)); } + public void testGetParams() { + Map> extractedFields = + Map.of( + "foo", Set.of(BooleanFieldMapper.CONTENT_TYPE), + "bar", Set.of(NumberFieldMapper.NumberType.LONG.typeName()), + "baz", Set.of(KeywordFieldMapper.CONTENT_TYPE)); + assertThat( + new Classification("foo").getParams(extractedFields), + equalTo( + Map.of( + "dependent_variable", "foo", + "num_top_classes", 2, + "prediction_field_name", "foo_prediction", + "prediction_field_type", "bool"))); + assertThat( + new Classification("bar").getParams(extractedFields), + equalTo( + Map.of( + "dependent_variable", "bar", + "num_top_classes", 2, + "prediction_field_name", "bar_prediction", + "prediction_field_type", "int"))); + assertThat( + new Classification("baz").getParams(extractedFields), + equalTo( + Map.of( + "dependent_variable", "baz", + "num_top_classes", 2, + "prediction_field_name", "baz_prediction", + "prediction_field_type", "string"))); + } + public void testFieldCardinalityLimitsIsNonNull() { assertThat(createTestInstance().getFieldCardinalityLimits(), is(not(nullValue()))); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetectionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetectionTests.java index b11817145174e..c35b9a3bad1af 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetectionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetectionTests.java @@ -51,7 +51,7 @@ protected Writeable.Reader instanceReader() { public void testGetParams_GivenDefaults() { OutlierDetection outlierDetection = new OutlierDetection.Builder().build(); - Map params = outlierDetection.getParams(); + Map params = outlierDetection.getParams(null); assertThat(params.size(), equalTo(3)); assertThat(params.containsKey("compute_feature_influence"), is(true)); assertThat(params.get("compute_feature_influence"), is(true)); @@ -71,7 +71,7 @@ public void testGetParams_GivenExplicitValues() { .setStandardizationEnabled(false) .build(); - Map params = outlierDetection.getParams(); + Map params = outlierDetection.getParams(null); assertThat(params.size(), equalTo(6)); assertThat(params.get(OutlierDetection.N_NEIGHBORS.getPreferredName()), equalTo(42)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java index 089f29e53cb5a..f3d5312280e88 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.test.AbstractSerializingTestCase; import java.io.IOException; +import java.util.Map; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -83,6 +84,12 @@ public void testGetTrainingPercent() { assertThat(regression.getTrainingPercent(), equalTo(100.0)); } + public void testGetParams() { + assertThat( + new Regression("foo").getParams(null), + equalTo(Map.of("dependent_variable", "foo", "prediction_field_name", "foo_prediction"))); + } + public void testFieldCardinalityLimitsIsNonNull() { assertThat(createTestInstance().getFieldCardinalityLimits(), is(not(nullValue()))); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/TypesTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/TypesTests.java new file mode 100644 index 0000000000000..beac3bcae9426 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/TypesTests.java @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.dataframe.analyses; + +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.Matchers.empty; + +public class TypesTests extends ESTestCase { + + public void testTypes() { + assertThat(Sets.intersection(Types.bool(), Types.categorical()), empty()); + assertThat(Sets.intersection(Types.categorical(), Types.numerical()), empty()); + assertThat(Sets.intersection(Types.numerical(), Types.bool()), empty()); + assertThat(Sets.difference(Types.discreteNumerical(), Types.numerical()), empty()); + } +} diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java index cb7ef69b5569e..6fd7e289fc231 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java @@ -27,8 +27,12 @@ public class ClassificationEvaluationIT extends MlNativeDataFrameAnalyticsIntegT private static final String ANIMALS_DATA_INDEX = "test-evaluate-animals-index"; - private static final String ACTUAL_CLASS_FIELD = "actual_class_field"; - private static final String PREDICTED_CLASS_FIELD = "predicted_class_field"; + private static final String ANIMAL_NAME_FIELD = "animal_name"; + private static final String ANIMAL_NAME_PREDICTION_FIELD = "animal_name_prediction"; + private static final String NO_LEGS_FIELD = "no_legs"; + private static final String NO_LEGS_PREDICTION_FIELD = "no_legs_prediction"; + private static final String IS_PREDATOR_FIELD = "predator"; + private static final String IS_PREDATOR_PREDICTION_FIELD = "predator_prediction"; @Before public void setup() { @@ -40,9 +44,9 @@ public void cleanup() { cleanUp(); } - public void testEvaluate_MulticlassClassification_DefaultMetrics() { + public void testEvaluate_DefaultMetrics() { EvaluateDataFrameAction.Response evaluateDataFrameResponse = - evaluateDataFrame(ANIMALS_DATA_INDEX, new Classification(ACTUAL_CLASS_FIELD, PREDICTED_CLASS_FIELD, null)); + evaluateDataFrame(ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, null)); assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); assertThat(evaluateDataFrameResponse.getMetrics(), hasSize(1)); @@ -51,9 +55,10 @@ public void testEvaluate_MulticlassClassification_DefaultMetrics() { equalTo(MulticlassConfusionMatrix.NAME.getPreferredName())); } - public void testEvaluate_MulticlassClassification_Accuracy() { + public void testEvaluate_Accuracy_KeywordField() { EvaluateDataFrameAction.Response evaluateDataFrameResponse = - evaluateDataFrame(ANIMALS_DATA_INDEX, new Classification(ACTUAL_CLASS_FIELD, PREDICTED_CLASS_FIELD, List.of(new Accuracy()))); + evaluateDataFrame( + ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new Accuracy()))); assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); assertThat(evaluateDataFrameResponse.getMetrics(), hasSize(1)); @@ -72,11 +77,50 @@ public void testEvaluate_MulticlassClassification_Accuracy() { assertThat(accuracyResult.getOverallAccuracy(), equalTo(5.0 / 75)); } - public void testEvaluate_MulticlassClassification_AccuracyAndConfusionMatrixMetricWithDefaultSize() { + public void testEvaluate_Accuracy_IntegerField() { + EvaluateDataFrameAction.Response evaluateDataFrameResponse = + evaluateDataFrame( + ANIMALS_DATA_INDEX, new Classification(NO_LEGS_FIELD, NO_LEGS_PREDICTION_FIELD, List.of(new Accuracy()))); + + assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); + assertThat(evaluateDataFrameResponse.getMetrics(), hasSize(1)); + + Accuracy.Result accuracyResult = (Accuracy.Result) evaluateDataFrameResponse.getMetrics().get(0); + assertThat(accuracyResult.getMetricName(), equalTo(Accuracy.NAME.getPreferredName())); + assertThat( + accuracyResult.getActualClasses(), + equalTo(List.of( + new Accuracy.ActualClass("1", 15, 1.0 / 15), + new Accuracy.ActualClass("2", 15, 2.0 / 15), + new Accuracy.ActualClass("3", 15, 3.0 / 15), + new Accuracy.ActualClass("4", 15, 4.0 / 15), + new Accuracy.ActualClass("5", 15, 5.0 / 15)))); + assertThat(accuracyResult.getOverallAccuracy(), equalTo(15.0 / 75)); + } + + public void testEvaluate_Accuracy_BooleanField() { + EvaluateDataFrameAction.Response evaluateDataFrameResponse = + evaluateDataFrame( + ANIMALS_DATA_INDEX, new Classification(IS_PREDATOR_FIELD, IS_PREDATOR_PREDICTION_FIELD, List.of(new Accuracy()))); + + assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); + assertThat(evaluateDataFrameResponse.getMetrics(), hasSize(1)); + + Accuracy.Result accuracyResult = (Accuracy.Result) evaluateDataFrameResponse.getMetrics().get(0); + assertThat(accuracyResult.getMetricName(), equalTo(Accuracy.NAME.getPreferredName())); + assertThat( + accuracyResult.getActualClasses(), + equalTo(List.of( + new Accuracy.ActualClass("true", 45, 27.0 / 45), + new Accuracy.ActualClass("false", 30, 18.0 / 30)))); + assertThat(accuracyResult.getOverallAccuracy(), equalTo(45.0 / 75)); + } + + public void testEvaluate_ConfusionMatrixMetricWithDefaultSize() { EvaluateDataFrameAction.Response evaluateDataFrameResponse = evaluateDataFrame( ANIMALS_DATA_INDEX, - new Classification(ACTUAL_CLASS_FIELD, PREDICTED_CLASS_FIELD, List.of(new MulticlassConfusionMatrix()))); + new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new MulticlassConfusionMatrix()))); assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); assertThat(evaluateDataFrameResponse.getMetrics(), hasSize(1)); @@ -135,11 +179,11 @@ public void testEvaluate_MulticlassClassification_AccuracyAndConfusionMatrixMetr assertThat(confusionMatrixResult.getOtherActualClassCount(), equalTo(0L)); } - public void testEvaluate_MulticlassClassification_ConfusionMatrixMetricWithUserProvidedSize() { + public void testEvaluate_ConfusionMatrixMetricWithUserProvidedSize() { EvaluateDataFrameAction.Response evaluateDataFrameResponse = evaluateDataFrame( ANIMALS_DATA_INDEX, - new Classification(ACTUAL_CLASS_FIELD, PREDICTED_CLASS_FIELD, List.of(new MulticlassConfusionMatrix(3)))); + new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new MulticlassConfusionMatrix(3)))); assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); assertThat(evaluateDataFrameResponse.getMetrics(), hasSize(1)); @@ -166,20 +210,30 @@ public void testEvaluate_MulticlassClassification_ConfusionMatrixMetricWithUserP private static void indexAnimalsData(String indexName) { client().admin().indices().prepareCreate(indexName) - .addMapping("_doc", ACTUAL_CLASS_FIELD, "type=keyword", PREDICTED_CLASS_FIELD, "type=keyword") + .addMapping("_doc", + ANIMAL_NAME_FIELD, "type=keyword", + ANIMAL_NAME_PREDICTION_FIELD, "type=keyword", + NO_LEGS_FIELD, "type=integer", + NO_LEGS_PREDICTION_FIELD, "type=integer", + IS_PREDATOR_FIELD, "type=boolean", + IS_PREDATOR_PREDICTION_FIELD, "type=boolean") .get(); - List classNames = List.of("dog", "cat", "mouse", "ant", "fox"); + List animalNames = List.of("dog", "cat", "mouse", "ant", "fox"); BulkRequestBuilder bulkRequestBuilder = client().prepareBulk() .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); - for (int i = 0; i < classNames.size(); i++) { - for (int j = 0; j < classNames.size(); j++) { + for (int i = 0; i < animalNames.size(); i++) { + for (int j = 0; j < animalNames.size(); j++) { for (int k = 0; k < j + 1; k++) { bulkRequestBuilder.add( new IndexRequest(indexName) .source( - ACTUAL_CLASS_FIELD, classNames.get(i), - PREDICTED_CLASS_FIELD, classNames.get((i + j) % classNames.size()))); + ANIMAL_NAME_FIELD, animalNames.get(i), + ANIMAL_NAME_PREDICTION_FIELD, animalNames.get((i + j) % animalNames.size()), + NO_LEGS_FIELD, i + 1, + NO_LEGS_PREDICTION_FIELD, j + 1, + IS_PREDATOR_FIELD, i % 2 == 0, + IS_PREDATOR_PREDICTION_FIELD, (i + j) % 2 == 0)); } } } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java index d354cf4cd4ba7..1d3f6bf4f2e75 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java @@ -20,9 +20,8 @@ import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.dataframe.analyses.BoostedTreeParamsTests; import org.elasticsearch.xpack.core.ml.dataframe.analyses.Classification; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.ActualClass; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.PredictedClass; import org.junit.After; import java.util.ArrayList; @@ -30,7 +29,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.function.Function; import static java.util.stream.Collectors.toList; import static org.hamcrest.Matchers.allOf; @@ -88,7 +86,7 @@ public void testSingleNumericFeatureAndMixedTrainingAndNonTrainingRows() throws assertThat((String) resultsObject.get("keyword-field_prediction"), is(in(KEYWORD_FIELD_VALUES))); assertThat(resultsObject.containsKey("is_training"), is(true)); assertThat(resultsObject.get("is_training"), is(destDoc.containsKey(KEYWORD_FIELD))); - assertTopClasses(resultsObject, 2, KEYWORD_FIELD, KEYWORD_FIELD_VALUES, String::valueOf); + assertTopClasses(resultsObject, 2, KEYWORD_FIELD, KEYWORD_FIELD_VALUES); } assertProgress(jobId, 100, 100, 100, 100); @@ -102,7 +100,7 @@ public void testSingleNumericFeatureAndMixedTrainingAndNonTrainingRows() throws "Creating destination index [" + destIndex + "]", "Finished reindexing to destination index [" + destIndex + "]", "Finished analysis"); - assertEvaluation(KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "ml.keyword-field_prediction"); + assertEvaluation(KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "ml.keyword-field_prediction.keyword"); } public void testWithOnlyTrainingRowsAndTrainingPercentIsHundred() throws Exception { @@ -128,7 +126,7 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsHundred() throws Excepti assertThat((String) resultsObject.get("keyword-field_prediction"), is(in(KEYWORD_FIELD_VALUES))); assertThat(resultsObject.containsKey("is_training"), is(true)); assertThat(resultsObject.get("is_training"), is(true)); - assertTopClasses(resultsObject, 2, KEYWORD_FIELD, KEYWORD_FIELD_VALUES, String::valueOf); + assertTopClasses(resultsObject, 2, KEYWORD_FIELD, KEYWORD_FIELD_VALUES); } assertProgress(jobId, 100, 100, 100, 100); @@ -142,11 +140,11 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsHundred() throws Excepti "Creating destination index [" + destIndex + "]", "Finished reindexing to destination index [" + destIndex + "]", "Finished analysis"); - assertEvaluation(KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "ml.keyword-field_prediction"); + assertEvaluation(KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "ml.keyword-field_prediction.keyword"); } public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty( - String jobId, String dependentVariable, List dependentVariableValues, Function parser) throws Exception { + String jobId, String dependentVariable, List dependentVariableValues) throws Exception { initialize(jobId); String predictedClassField = dependentVariable + "_prediction"; indexData(sourceIndex, 300, 0, dependentVariable); @@ -175,9 +173,10 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty( for (SearchHit hit : sourceData.getHits()) { Map resultsObject = getMlResultsObjectFromDestDoc(getDestDoc(config, hit)); assertThat(resultsObject.containsKey(predictedClassField), is(true)); - T predictedClassValue = parser.apply((String) resultsObject.get(predictedClassField)); + @SuppressWarnings("unchecked") + T predictedClassValue = (T) resultsObject.get(predictedClassField); assertThat(predictedClassValue, is(in(dependentVariableValues))); - assertTopClasses(resultsObject, numTopClasses, dependentVariable, dependentVariableValues, parser); + assertTopClasses(resultsObject, numTopClasses, dependentVariable, dependentVariableValues); assertThat(resultsObject.containsKey("is_training"), is(true)); // Let's just assert there's both training and non-training results @@ -201,33 +200,32 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty( "Creating destination index [" + destIndex + "]", "Finished reindexing to destination index [" + destIndex + "]", "Finished analysis"); - assertEvaluation( - dependentVariable, - dependentVariableValues.stream().map(String::valueOf).collect(toList()), - "ml." + predictedClassField); } public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty_DependentVariableIsKeyword() throws Exception { testWithOnlyTrainingRowsAndTrainingPercentIsFifty( - "classification_training_percent_is_50_keyword", KEYWORD_FIELD, KEYWORD_FIELD_VALUES, String::valueOf); + "classification_training_percent_is_50_keyword", KEYWORD_FIELD, KEYWORD_FIELD_VALUES); + assertEvaluation(KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "ml.keyword-field_prediction.keyword"); } public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty_DependentVariableIsInteger() throws Exception { testWithOnlyTrainingRowsAndTrainingPercentIsFifty( - "classification_training_percent_is_50_integer", DISCRETE_NUMERICAL_FIELD, DISCRETE_NUMERICAL_FIELD_VALUES, Integer::valueOf); + "classification_training_percent_is_50_integer", DISCRETE_NUMERICAL_FIELD, DISCRETE_NUMERICAL_FIELD_VALUES); + assertEvaluation(DISCRETE_NUMERICAL_FIELD, DISCRETE_NUMERICAL_FIELD_VALUES, "ml.discrete-numerical-field_prediction"); } public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty_DependentVariableIsDouble() throws Exception { ElasticsearchStatusException e = expectThrows( ElasticsearchStatusException.class, () -> testWithOnlyTrainingRowsAndTrainingPercentIsFifty( - "classification_training_percent_is_50_double", NUMERICAL_FIELD, NUMERICAL_FIELD_VALUES, Double::valueOf)); + "classification_training_percent_is_50_double", NUMERICAL_FIELD, NUMERICAL_FIELD_VALUES)); assertThat(e.getMessage(), startsWith("invalid types [double] for required field [numerical-field];")); } public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty_DependentVariableIsBoolean() throws Exception { testWithOnlyTrainingRowsAndTrainingPercentIsFifty( - "classification_training_percent_is_50_boolean", BOOLEAN_FIELD, BOOLEAN_FIELD_VALUES, Boolean::valueOf); + "classification_training_percent_is_50_boolean", BOOLEAN_FIELD, BOOLEAN_FIELD_VALUES); + assertEvaluation(BOOLEAN_FIELD, BOOLEAN_FIELD_VALUES, "ml.boolean-field_prediction"); } public void testDependentVariableCardinalityTooHighError() { @@ -317,25 +315,24 @@ private static Map getMlResultsObjectFromDestDoc(Map void assertTopClasses( Map resultsObject, int numTopClasses, String dependentVariable, - List dependentVariableValues, - Function parser) { + List dependentVariableValues) { assertThat(resultsObject.containsKey("top_classes"), is(true)); - @SuppressWarnings("unchecked") List> topClasses = (List>) resultsObject.get("top_classes"); assertThat(topClasses, hasSize(numTopClasses)); - List classNames = new ArrayList<>(topClasses.size()); + List classNames = new ArrayList<>(topClasses.size()); List classProbabilities = new ArrayList<>(topClasses.size()); for (Map topClass : topClasses) { assertThat(topClass, allOf(hasKey("class_name"), hasKey("class_probability"))); - classNames.add((String) topClass.get("class_name")); + classNames.add((T) topClass.get("class_name")); classProbabilities.add((Double) topClass.get("class_probability")); } // Assert that all the predicted class names come from the set of dependent variable values. - classNames.forEach(className -> assertThat(parser.apply(className), is(in(dependentVariableValues)))); + classNames.forEach(className -> assertThat(className, is(in(dependentVariableValues)))); // Assert that the first class listed in top classes is the same as the predicted class. assertThat(classNames.get(0), equalTo(resultsObject.get(dependentVariable + "_prediction"))); // Assert that all the class probabilities lie within [0, 1] interval. @@ -344,25 +341,44 @@ private static void assertTopClasses( assertThat(Ordering.natural().reverse().isOrdered(classProbabilities), is(true)); } - private void assertEvaluation(String dependentVariable, List dependentVariableValues, String predictedClassField) { + private void assertEvaluation(String dependentVariable, List dependentVariableValues, String predictedClassField) { + List dependentVariableValuesAsStrings = dependentVariableValues.stream().map(String::valueOf).collect(toList()); EvaluateDataFrameAction.Response evaluateDataFrameResponse = evaluateDataFrame( destIndex, new org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Classification( - dependentVariable, predictedClassField, null)); + dependentVariable, predictedClassField, Arrays.asList(new Accuracy(), new MulticlassConfusionMatrix()))); assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); - assertThat(evaluateDataFrameResponse.getMetrics().size(), equalTo(1)); - MulticlassConfusionMatrix.Result confusionMatrixResult = - (MulticlassConfusionMatrix.Result) evaluateDataFrameResponse.getMetrics().get(0); - assertThat(confusionMatrixResult.getMetricName(), equalTo(MulticlassConfusionMatrix.NAME.getPreferredName())); - List actualClasses = confusionMatrixResult.getConfusionMatrix(); - assertThat(actualClasses.stream().map(ActualClass::getActualClass).collect(toList()), equalTo(dependentVariableValues)); - for (ActualClass actualClass : actualClasses) { - assertThat(actualClass.getOtherPredictedClassDocCount(), equalTo(0L)); + assertThat(evaluateDataFrameResponse.getMetrics().size(), equalTo(2)); + + { // Accuracy + Accuracy.Result accuracyResult = (Accuracy.Result) evaluateDataFrameResponse.getMetrics().get(0); + assertThat(accuracyResult.getMetricName(), equalTo(Accuracy.NAME.getPreferredName())); + List actualClasses = accuracyResult.getActualClasses(); + assertThat( + actualClasses.stream().map(Accuracy.ActualClass::getActualClass).collect(toList()), + equalTo(dependentVariableValuesAsStrings)); + actualClasses.forEach( + actualClass -> assertThat(actualClass.getAccuracy(), allOf(greaterThanOrEqualTo(0.0), lessThanOrEqualTo(1.0)))); + } + + { // MulticlassConfusionMatrix + MulticlassConfusionMatrix.Result confusionMatrixResult = + (MulticlassConfusionMatrix.Result) evaluateDataFrameResponse.getMetrics().get(1); + assertThat(confusionMatrixResult.getMetricName(), equalTo(MulticlassConfusionMatrix.NAME.getPreferredName())); + List actualClasses = confusionMatrixResult.getConfusionMatrix(); assertThat( - actualClass.getPredictedClasses().stream().map(PredictedClass::getPredictedClass).collect(toList()), - equalTo(dependentVariableValues)); + actualClasses.stream().map(MulticlassConfusionMatrix.ActualClass::getActualClass).collect(toList()), + equalTo(dependentVariableValuesAsStrings)); + for (MulticlassConfusionMatrix.ActualClass actualClass : actualClasses) { + assertThat(actualClass.getOtherPredictedClassDocCount(), equalTo(0L)); + assertThat( + actualClass.getPredictedClasses().stream() + .map(MulticlassConfusionMatrix.PredictedClass::getPredictedClass) + .collect(toList()), + equalTo(dependentVariableValuesAsStrings)); + } + assertThat(confusionMatrixResult.getOtherActualClassCount(), equalTo(0L)); } - assertThat(confusionMatrixResult.getOtherActualClassCount(), equalTo(0L)); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java index 1c060f178644d..c7d27805c3b4e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java @@ -56,6 +56,10 @@ public DataFrameDataExtractor newExtractor(boolean includeSource) { return new DataFrameDataExtractor(client, context); } + public ExtractedFields getExtractedFields() { + return extractedFields; + } + private QueryBuilder createQuery() { BoolQueryBuilder query = QueryBuilders.boolQuery(); query.filter(sourceQuery); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java index 62184e290374d..a98d2cb40543b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java @@ -379,14 +379,12 @@ private ExtractedFields fetchBooleanFieldsAsIntegers(ExtractedFields extractedFi List adjusted = new ArrayList<>(extractedFields.getAllFields().size()); for (ExtractedField field : extractedFields.getAllFields()) { if (isBoolean(field.getTypes())) { - if (config.getAnalysis().getAllowedCategoricalTypes(field.getName()).contains(BooleanFieldMapper.CONTENT_TYPE)) { - // We convert boolean field to string if it is a categorical dependent variable - adjusted.add(ExtractedFields.applyBooleanMapping(field, Boolean.TRUE.toString(), Boolean.FALSE.toString())); - } else { - // We convert boolean fields to integers with values 0, 1 as this is the preferred - // way to consume such features in the analytics process. - adjusted.add(ExtractedFields.applyBooleanMapping(field, 1, 0)); - } + // We convert boolean fields to integers with values 0, 1 as this is the preferred + // way to consume such features in the analytics process regardless of: + // - analysis type + // - whether or not the field is categorical + // - whether or not the field is a dependent variable + adjusted.add(ExtractedFields.applyBooleanMapping(field)); } else { adjusted.add(field); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessConfig.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessConfig.java index 9a172d158e512..714f63091801f 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessConfig.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessConfig.java @@ -9,11 +9,15 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.xpack.core.ml.dataframe.analyses.DataFrameAnalysis; +import org.elasticsearch.xpack.ml.extractor.ExtractedField; +import org.elasticsearch.xpack.ml.extractor.ExtractedFields; import java.io.IOException; import java.util.Objects; import java.util.Set; +import static java.util.stream.Collectors.toMap; + public class AnalyticsProcessConfig implements ToXContentObject { private static final String JOB_ID = "job_id"; @@ -33,9 +37,10 @@ public class AnalyticsProcessConfig implements ToXContentObject { private final String resultsField; private final Set categoricalFields; private final DataFrameAnalysis analysis; + private final ExtractedFields extractedFields; public AnalyticsProcessConfig(String jobId, long rows, int cols, ByteSizeValue memoryLimit, int threads, String resultsField, - Set categoricalFields, DataFrameAnalysis analysis) { + Set categoricalFields, DataFrameAnalysis analysis, ExtractedFields extractedFields) { this.jobId = Objects.requireNonNull(jobId); this.rows = rows; this.cols = cols; @@ -44,6 +49,7 @@ public AnalyticsProcessConfig(String jobId, long rows, int cols, ByteSizeValue m this.resultsField = Objects.requireNonNull(resultsField); this.categoricalFields = Objects.requireNonNull(categoricalFields); this.analysis = Objects.requireNonNull(analysis); + this.extractedFields = Objects.requireNonNull(extractedFields); } public String jobId() { @@ -68,7 +74,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(THREADS, threads); builder.field(RESULTS_FIELD, resultsField); builder.field(CATEGORICAL_FIELDS, categoricalFields); - builder.field(ANALYSIS, new DataFrameAnalysisWrapper(analysis)); + builder.field(ANALYSIS, new DataFrameAnalysisWrapper(analysis, extractedFields)); builder.endObject(); return builder; } @@ -76,16 +82,21 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws private static class DataFrameAnalysisWrapper implements ToXContentObject { private final DataFrameAnalysis analysis; + private final ExtractedFields extractedFields; - private DataFrameAnalysisWrapper(DataFrameAnalysis analysis) { + private DataFrameAnalysisWrapper(DataFrameAnalysis analysis, ExtractedFields extractedFields) { this.analysis = analysis; + this.extractedFields = extractedFields; } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field("name", analysis.getWriteableName()); - builder.field("parameters", analysis.getParams()); + builder.field( + "parameters", + analysis.getParams( + extractedFields.getAllFields().stream().collect(toMap(ExtractedField::getName, ExtractedField::getTypes)))); builder.endObject(); return builder; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java index ed9d715b5f78c..815d8478a5275 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java @@ -33,6 +33,7 @@ import org.elasticsearch.xpack.ml.dataframe.process.customprocessing.CustomProcessor; import org.elasticsearch.xpack.ml.dataframe.process.customprocessing.CustomProcessorFactory; import org.elasticsearch.xpack.ml.dataframe.process.results.AnalyticsResult; +import org.elasticsearch.xpack.ml.extractor.ExtractedFields; import org.elasticsearch.xpack.ml.inference.persistence.TrainedModelProvider; import org.elasticsearch.xpack.ml.notifications.DataFrameAnalyticsAuditor; @@ -373,7 +374,8 @@ synchronized boolean startProcess(DataFrameDataExtractorFactory dataExtractorFac } dataExtractor = dataExtractorFactory.newExtractor(false); - AnalyticsProcessConfig analyticsProcessConfig = createProcessConfig(config, dataExtractor); + AnalyticsProcessConfig analyticsProcessConfig = + createProcessConfig(config, dataExtractor, dataExtractorFactory.getExtractedFields()); LOGGER.trace("[{}] creating analytics process with config [{}]", config.getId(), Strings.toString(analyticsProcessConfig)); // If we have no rows, that means there is no data so no point in starting the native process // just finish the task @@ -389,11 +391,20 @@ synchronized boolean startProcess(DataFrameDataExtractorFactory dataExtractorFac return true; } - private AnalyticsProcessConfig createProcessConfig(DataFrameAnalyticsConfig config, DataFrameDataExtractor dataExtractor) { + private AnalyticsProcessConfig createProcessConfig( + DataFrameAnalyticsConfig config, DataFrameDataExtractor dataExtractor, ExtractedFields extractedFields) { DataFrameDataExtractor.DataSummary dataSummary = dataExtractor.collectDataSummary(); Set categoricalFields = dataExtractor.getCategoricalFields(config.getAnalysis()); - AnalyticsProcessConfig processConfig = new AnalyticsProcessConfig(config.getId(), dataSummary.rows, dataSummary.cols, - config.getModelMemoryLimit(), 1, config.getDest().getResultsField(), categoricalFields, config.getAnalysis()); + AnalyticsProcessConfig processConfig = new AnalyticsProcessConfig( + config.getId(), + dataSummary.rows, + dataSummary.cols, + config.getModelMemoryLimit(), + 1, + config.getDest().getResultsField(), + categoricalFields, + config.getAnalysis(), + extractedFields); return processConfig; } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/MemoryUsageEstimationProcessManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/MemoryUsageEstimationProcessManager.java index 6740f8d4d34ca..a6223ec8b8863 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/MemoryUsageEstimationProcessManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/MemoryUsageEstimationProcessManager.java @@ -79,7 +79,8 @@ private MemoryUsageEstimationResult runJob(String jobId, 1, "", categoricalFields, - config.getAnalysis()); + config.getAnalysis(), + dataExtractorFactory.getExtractedFields()); AnalyticsProcess process = processFactory.createAnalyticsProcess( config, diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/extractor/ExtractedFields.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/extractor/ExtractedFields.java index 9fe079b745c10..347d353664dfa 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/extractor/ExtractedFields.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/extractor/ExtractedFields.java @@ -62,8 +62,8 @@ public static TimeField newTimeField(String name, ExtractedField.Method method) return new TimeField(name, method); } - public static ExtractedField applyBooleanMapping(ExtractedField field, T trueValue, T falseValue) { - return new BooleanMapper<>(field, trueValue, falseValue); + public static ExtractedField applyBooleanMapping(ExtractedField field) { + return new BooleanMapper<>(field, 1, 0); } public static class ExtractionMethodDetector { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java index f4f25bcfa0636..9c55b2a9ac956 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java @@ -36,6 +36,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -57,7 +58,7 @@ public void testDetect_GivenFloatField() { Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); - assertThat(allFields.size(), equalTo(1)); + assertThat(allFields, hasSize(1)); assertThat(allFields.get(0).getName(), equalTo("some_float")); assertThat(allFields.get(0).getMethod(), equalTo(ExtractedField.Method.DOC_VALUE)); @@ -75,7 +76,7 @@ public void testDetect_GivenNumericFieldWithMultipleTypes() { Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); - assertThat(allFields.size(), equalTo(1)); + assertThat(allFields, hasSize(1)); assertThat(allFields.get(0).getName(), equalTo("some_number")); assertThat(allFields.get(0).getMethod(), equalTo(ExtractedField.Method.DOC_VALUE)); @@ -121,7 +122,7 @@ public void testDetect_GivenOutlierDetectionAndMultipleFields() { Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); - assertThat(allFields.size(), equalTo(3)); + assertThat(allFields, hasSize(3)); assertThat(allFields.stream().map(ExtractedField::getName).collect(Collectors.toSet()), containsInAnyOrder("some_float", "some_long", "some_boolean")); assertThat(allFields.stream().map(ExtractedField::getMethod).collect(Collectors.toSet()), @@ -150,7 +151,7 @@ public void testDetect_GivenRegressionAndMultipleFields() { Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); - assertThat(allFields.size(), equalTo(5)); + assertThat(allFields, hasSize(5)); assertThat(allFields.stream().map(ExtractedField::getName).collect(Collectors.toList()), containsInAnyOrder("foo", "some_float", "some_keyword", "some_long", "some_boolean")); assertThat(allFields.stream().map(ExtractedField::getMethod).collect(Collectors.toSet()), @@ -223,7 +224,7 @@ public void testDetect_GivenFieldIsBothIncludedAndExcluded() { Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); - assertThat(allFields.size(), equalTo(1)); + assertThat(allFields, hasSize(1)); assertThat(allFields.stream().map(ExtractedField::getName).collect(Collectors.toList()), contains("bar")); assertFieldSelectionContains(fieldExtraction.v2(), @@ -329,7 +330,7 @@ public void testDetect_GivenExcludedFieldIsUnsupported() { Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); - assertThat(allFields.size(), equalTo(1)); + assertThat(allFields, hasSize(1)); assertThat(allFields.get(0).getName(), equalTo("numeric")); assertFieldSelectionContains(fieldExtraction.v2(), @@ -565,23 +566,24 @@ public void testDetect_GivenMoreFieldsThanDocValuesLimit() { contains(equalTo(ExtractedField.Method.SOURCE))); } - public void testDetect_GivenBooleanField_BooleanMappedAsInteger() { + private void testDetect_GivenBooleanField(DataFrameAnalyticsConfig config, boolean isRequired, FieldSelection.FeatureType featureType) { FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() .addAggregatableField("some_boolean", "boolean") + .addAggregatableField("some_integer", "integer") .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, config, false, 100, fieldCapabilities, config.getAnalysis().getFieldCardinalityLimits()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); - assertThat(allFields.size(), equalTo(1)); + assertThat(allFields, hasSize(2)); ExtractedField booleanField = allFields.get(0); assertThat(booleanField.getTypes(), contains("boolean")); assertThat(booleanField.getMethod(), equalTo(ExtractedField.Method.DOC_VALUE)); - assertFieldSelectionContains(fieldExtraction.v2(), - FieldSelection.included("some_boolean", Collections.singleton("boolean"), false, FieldSelection.FeatureType.NUMERICAL) + assertFieldSelectionContains(fieldExtraction.v2().subList(0, 1), + FieldSelection.included("some_boolean", Collections.singleton("boolean"), isRequired, featureType) ); SearchHit hit = new SearchHitBuilder(42).addField("some_boolean", true).build(); @@ -594,34 +596,24 @@ public void testDetect_GivenBooleanField_BooleanMappedAsInteger() { assertThat(booleanField.value(hit), arrayContaining(0, 1, 0)); } - public void testDetect_GivenBooleanField_BooleanMappedAsString() { - FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() - .addAggregatableField("some_boolean", "boolean") - .build(); - - ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildClassificationConfig("some_boolean"), false, 100, fieldCapabilities, - Collections.singletonMap("some_boolean", 2L)); - Tuple> fieldExtraction = extractedFieldsDetector.detect(); - - List allFields = fieldExtraction.v1().getAllFields(); - assertThat(allFields.size(), equalTo(1)); - ExtractedField booleanField = allFields.get(0); - assertThat(booleanField.getTypes(), contains("boolean")); - assertThat(booleanField.getMethod(), equalTo(ExtractedField.Method.DOC_VALUE)); - - assertFieldSelectionContains(fieldExtraction.v2(), - FieldSelection.included("some_boolean", Collections.singleton("boolean"), true, FieldSelection.FeatureType.CATEGORICAL) - ); + public void testDetect_GivenBooleanField_OutlierDetection() { + // some_boolean is a non-required, numerical feature in outlier detection analysis + testDetect_GivenBooleanField(buildOutlierDetectionConfig(), false, FieldSelection.FeatureType.NUMERICAL); + } - SearchHit hit = new SearchHitBuilder(42).addField("some_boolean", true).build(); - assertThat(booleanField.value(hit), arrayContaining("true")); + public void testDetect_GivenBooleanField_Regression() { + // some_boolean is a non-required, numerical feature in regression analysis + testDetect_GivenBooleanField(buildRegressionConfig("some_integer"), false, FieldSelection.FeatureType.NUMERICAL); + } - hit = new SearchHitBuilder(42).addField("some_boolean", false).build(); - assertThat(booleanField.value(hit), arrayContaining("false")); + public void testDetect_GivenBooleanField_Classification_BooleanIsFeature() { + // some_boolean is a non-required, numerical feature in classification analysis + testDetect_GivenBooleanField(buildClassificationConfig("some_integer"), false, FieldSelection.FeatureType.NUMERICAL); + } - hit = new SearchHitBuilder(42).addField("some_boolean", Arrays.asList(false, true, false)).build(); - assertThat(booleanField.value(hit), arrayContaining("false", "true", "false")); + public void testDetect_GivenBooleanField_Classification_BooleanIsDependentVariable() { + // some_boolean is a required, categorical dependent variable in classification analysis + testDetect_GivenBooleanField(buildClassificationConfig("some_boolean"), true, FieldSelection.FeatureType.CATEGORICAL); } public void testDetect_GivenMultiFields() { @@ -640,7 +632,7 @@ public void testDetect_GivenMultiFields() { SOURCE_INDEX, buildRegressionConfig("a_float"), true, 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); - assertThat(fieldExtraction.v1().getAllFields().size(), equalTo(5)); + assertThat(fieldExtraction.v1().getAllFields(), hasSize(5)); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) .collect(Collectors.toList()); assertThat(extractedFieldNames, contains("a_float", "keyword_1", "text_1.keyword", "text_2.keyword", "text_without_keyword")); @@ -671,7 +663,7 @@ public void testDetect_GivenMultiFieldAndParentIsRequired() { SOURCE_INDEX, buildClassificationConfig("field_1"), true, 100, fieldCapabilities, Collections.singletonMap("field_1", 2L)); Tuple> fieldExtraction = extractedFieldsDetector.detect(); - assertThat(fieldExtraction.v1().getAllFields().size(), equalTo(2)); + assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) .collect(Collectors.toList()); assertThat(extractedFieldNames, contains("field_1", "field_2")); @@ -696,7 +688,7 @@ SOURCE_INDEX, buildClassificationConfig("field_1.keyword"), true, 100, fieldCapa Collections.singletonMap("field_1.keyword", 2L)); Tuple> fieldExtraction = extractedFieldsDetector.detect(); - assertThat(fieldExtraction.v1().getAllFields().size(), equalTo(2)); + assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) .collect(Collectors.toList()); assertThat(extractedFieldNames, contains("field_1.keyword", "field_2")); @@ -722,7 +714,7 @@ public void testDetect_GivenSeveralMultiFields_ShouldPickFirstSorted() { SOURCE_INDEX, buildRegressionConfig("field_2"), true, 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); - assertThat(fieldExtraction.v1().getAllFields().size(), equalTo(2)); + assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) .collect(Collectors.toList()); assertThat(extractedFieldNames, contains("field_1.keyword_1", "field_2")); @@ -748,7 +740,7 @@ public void testDetect_GivenMultiFields_OverDocValueLimit() { SOURCE_INDEX, buildRegressionConfig("field_2"), true, 0, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); - assertThat(fieldExtraction.v1().getAllFields().size(), equalTo(2)); + assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) .collect(Collectors.toList()); assertThat(extractedFieldNames, contains("field_1", "field_2")); @@ -773,7 +765,7 @@ public void testDetect_GivenParentAndMultiFieldBothAggregatable() { SOURCE_INDEX, buildRegressionConfig("field_2.double"), true, 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); - assertThat(fieldExtraction.v1().getAllFields().size(), equalTo(2)); + assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) .collect(Collectors.toList()); assertThat(extractedFieldNames, contains("field_1", "field_2.double")); @@ -798,7 +790,7 @@ public void testDetect_GivenParentAndMultiFieldNoneAggregatable() { SOURCE_INDEX, buildRegressionConfig("field_2"), true, 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); - assertThat(fieldExtraction.v1().getAllFields().size(), equalTo(2)); + assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) .collect(Collectors.toList()); assertThat(extractedFieldNames, contains("field_1", "field_2")); @@ -823,7 +815,7 @@ public void testDetect_GivenMultiFields_AndExplicitlyIncludedFields() { SOURCE_INDEX, buildRegressionConfig("field_2"), false, 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); - assertThat(fieldExtraction.v1().getAllFields().size(), equalTo(2)); + assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) .collect(Collectors.toList()); assertThat(extractedFieldNames, contains("field_1", "field_2")); @@ -849,7 +841,7 @@ public void testDetect_GivenSourceFilteringWithIncludes() { Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); - assertThat(allFields.size(), equalTo(2)); + assertThat(allFields, hasSize(2)); assertThat(allFields.get(0).getName(), equalTo("field_11")); assertThat(allFields.get(1).getName(), equalTo("field_12")); @@ -872,7 +864,7 @@ public void testDetect_GivenSourceFilteringWithExcludes() { Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); - assertThat(allFields.size(), equalTo(2)); + assertThat(allFields, hasSize(2)); assertThat(allFields.get(0).getName(), equalTo("field_21")); assertThat(allFields.get(1).getName(), equalTo("field_22")); @@ -914,7 +906,7 @@ private DataFrameAnalyticsConfig buildClassificationConfig(String dependentVaria * We assert each field individually to get useful error messages in case of failure */ private static void assertFieldSelectionContains(List actual, FieldSelection... expected) { - assertThat(actual.size(), equalTo(expected.length)); + assertThat(actual, hasSize(expected.length)); for (int i = 0; i < expected.length; i++) { assertThat("i = " + i, actual.get(i).getName(), equalTo(expected[i].getName())); assertThat("i = " + i, actual.get(i).getMappingTypes(), equalTo(expected[i].getMappingTypes())); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManagerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManagerTests.java index d86f0397319bb..4a0d5fa7f36df 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManagerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManagerTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.ml.dataframe.extractor.DataFrameDataExtractor; import org.elasticsearch.xpack.ml.dataframe.extractor.DataFrameDataExtractorFactory; import org.elasticsearch.xpack.ml.dataframe.process.results.AnalyticsResult; +import org.elasticsearch.xpack.ml.extractor.ExtractedFields; import org.elasticsearch.xpack.ml.inference.persistence.TrainedModelProvider; import org.elasticsearch.xpack.ml.notifications.DataFrameAnalyticsAuditor; import org.junit.Before; @@ -95,6 +96,7 @@ public void setUpMocks() { when(dataExtractor.collectDataSummary()).thenReturn(new DataFrameDataExtractor.DataSummary(NUM_ROWS, NUM_COLS)); dataExtractorFactory = mock(DataFrameDataExtractorFactory.class); when(dataExtractorFactory.newExtractor(anyBoolean())).thenReturn(dataExtractor); + when(dataExtractorFactory.getExtractedFields()).thenReturn(mock(ExtractedFields.class)); finishHandler = mock(Consumer.class); exceptionCaptor = ArgumentCaptor.forClass(Exception.class); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java index b1a2ba226b49f..15bd32da3c320 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.xpack.ml.dataframe.DataFrameAnalyticsTask.ProgressTracker; import org.elasticsearch.xpack.ml.dataframe.process.results.AnalyticsResult; import org.elasticsearch.xpack.ml.dataframe.process.results.RowResults; +import org.elasticsearch.xpack.ml.extractor.ExtractedFields; import org.elasticsearch.xpack.ml.inference.persistence.TrainedModelProvider; import org.elasticsearch.xpack.ml.notifications.DataFrameAnalyticsAuditor; import org.junit.Before; @@ -219,7 +220,8 @@ private void givenProcessResults(List results) { private void givenDataFrameRows(int rows) { AnalyticsProcessConfig config = new AnalyticsProcessConfig( - "job_id", rows, 1, ByteSizeValue.ZERO, 1, "ml", Collections.emptySet(), mock(DataFrameAnalysis.class)); + "job_id", rows, 1, ByteSizeValue.ZERO, 1, "ml", Collections.emptySet(), mock(DataFrameAnalysis.class), + mock(ExtractedFields.class)); when(process.getConfig()).thenReturn(config); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/MemoryUsageEstimationProcessManagerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/MemoryUsageEstimationProcessManagerTests.java index 5dc015d86e715..b2898667d036e 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/MemoryUsageEstimationProcessManagerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/MemoryUsageEstimationProcessManagerTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.ml.dataframe.extractor.DataFrameDataExtractor; import org.elasticsearch.xpack.ml.dataframe.extractor.DataFrameDataExtractorFactory; import org.elasticsearch.xpack.ml.dataframe.process.results.MemoryUsageEstimationResult; +import org.elasticsearch.xpack.ml.extractor.ExtractedFields; import org.junit.Before; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; @@ -70,6 +71,7 @@ public void setUpMocks() { when(dataExtractor.collectDataSummary()).thenReturn(new DataFrameDataExtractor.DataSummary(NUM_ROWS, NUM_COLS)); dataExtractorFactory = mock(DataFrameDataExtractorFactory.class); when(dataExtractorFactory.newExtractor(anyBoolean())).thenReturn(dataExtractor); + when(dataExtractorFactory.getExtractedFields()).thenReturn(mock(ExtractedFields.class)); dataFrameAnalyticsConfig = DataFrameAnalyticsConfigTests.createRandom(CONFIG_ID); listener = mock(ActionListener.class); resultCaptor = ArgumentCaptor.forClass(MemoryUsageEstimationResult.class); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/extractor/ExtractedFieldsTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/extractor/ExtractedFieldsTests.java index 9613d14fb5f00..5ac983e7d505b 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/extractor/ExtractedFieldsTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/extractor/ExtractedFieldsTests.java @@ -101,7 +101,7 @@ public void testBuildGivenMultiFields() { public void testApplyBooleanMapping() { DocValueField aBool = new DocValueField("a_bool", Collections.singleton("boolean")); - ExtractedField mapped = ExtractedFields.applyBooleanMapping(aBool, 1, 0); + ExtractedField mapped = ExtractedFields.applyBooleanMapping(aBool); SearchHit hitTrue = new SearchHitBuilder(42).addField("a_bool", true).build(); SearchHit hitFalse = new SearchHitBuilder(42).addField("a_bool", false).build(); From dde9e5980d3d3eb9dedab8117f5945ab146dbfbb Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Mon, 9 Dec 2019 07:28:08 -0500 Subject: [PATCH 119/686] [ML][Inference] adjust so target_field always has inference result and optionally allow new top classes field in the classification config (#49923) --- .../ClassificationInferenceResults.java | 14 +++--- .../inference/results/InferenceResults.java | 3 +- .../results/RawInferenceResults.java | 3 +- .../results/RegressionInferenceResults.java | 3 +- .../trainedmodel/ClassificationConfig.java | 24 ++++++++-- .../ClassificationInferenceResultsTests.java | 11 +++-- .../RegressionInferenceResultsTests.java | 3 +- .../ClassificationConfigTests.java | 15 ++++--- .../ml/integration/InferenceIngestIT.java | 16 +------ .../inference/ingest/InferenceProcessor.java | 10 ++--- .../ingest/InferenceProcessorTests.java | 44 ++++++++++++++++--- .../integration/ModelInferenceActionIT.java | 6 +-- 12 files changed, 102 insertions(+), 50 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResults.java index 662585bedf51d..4df54e3fa9eca 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResults.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResults.java @@ -12,6 +12,8 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ClassificationConfig; +import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.io.IOException; @@ -90,13 +92,15 @@ public String valueAsString() { } @Override - public void writeResult(IngestDocument document, String resultField) { + public void writeResult(IngestDocument document, String resultField, InferenceConfig config) { + assert config instanceof ClassificationConfig; + ClassificationConfig classificationConfig = (ClassificationConfig)config; ExceptionsHelper.requireNonNull(document, "document"); ExceptionsHelper.requireNonNull(resultField, "resultField"); - if (topClasses.isEmpty()) { - document.setFieldValue(resultField, valueAsString()); - } else { - document.setFieldValue(resultField, topClasses.stream().map(TopClassEntry::asValueMap).collect(Collectors.toList())); + document.setFieldValue(resultField, valueAsString()); + if (topClasses.isEmpty() == false) { + document.setFieldValue(classificationConfig.getTopClassesResultsField(), + topClasses.stream().map(TopClassEntry::asValueMap).collect(Collectors.toList())); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/InferenceResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/InferenceResults.java index 00744f6982f46..357fdcda386df 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/InferenceResults.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/InferenceResults.java @@ -7,10 +7,11 @@ import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; import org.elasticsearch.xpack.core.ml.utils.NamedXContentObject; public interface InferenceResults extends NamedXContentObject, NamedWriteable { - void writeResult(IngestDocument document, String resultField); + void writeResult(IngestDocument document, String resultField, InferenceConfig config); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RawInferenceResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RawInferenceResults.java index 884d66032b564..bb72483fe4878 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RawInferenceResults.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RawInferenceResults.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; import java.io.IOException; import java.util.Objects; @@ -49,7 +50,7 @@ public int hashCode() { } @Override - public void writeResult(IngestDocument document, String resultField) { + public void writeResult(IngestDocument document, String resultField, InferenceConfig config) { throw new UnsupportedOperationException("[raw] does not support writing inference results"); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResults.java index e186489b91dab..0fb931cd4a251 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResults.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResults.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.io.IOException; @@ -50,7 +51,7 @@ public int hashCode() { } @Override - public void writeResult(IngestDocument document, String resultField) { + public void writeResult(IngestDocument document, String resultField, InferenceConfig config) { ExceptionsHelper.requireNonNull(document, "document"); ExceptionsHelper.requireNonNull(resultField, "resultField"); document.setFieldValue(resultField, value()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfig.java index f7da41eda7bd6..3923ef9313190 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfig.java @@ -21,37 +21,52 @@ public class ClassificationConfig implements InferenceConfig { public static final String NAME = "classification"; + public static final String DEFAULT_TOP_CLASSES_RESULT_FIELD = "top_classes"; public static final ParseField NUM_TOP_CLASSES = new ParseField("num_top_classes"); + public static final ParseField TOP_CLASSES_RESULT_FIELD = new ParseField("top_classes_result_field"); private static final Version MIN_SUPPORTED_VERSION = Version.V_7_6_0; - public static ClassificationConfig EMPTY_PARAMS = new ClassificationConfig(0); + public static ClassificationConfig EMPTY_PARAMS = new ClassificationConfig(0, DEFAULT_TOP_CLASSES_RESULT_FIELD); private final int numTopClasses; + private final String topClassesResultsField; public static ClassificationConfig fromMap(Map map) { Map options = new HashMap<>(map); Integer numTopClasses = (Integer)options.remove(NUM_TOP_CLASSES.getPreferredName()); + String topClassesResultsField = (String)options.remove(TOP_CLASSES_RESULT_FIELD.getPreferredName()); if (options.isEmpty() == false) { throw ExceptionsHelper.badRequestException("Unrecognized fields {}.", options.keySet()); } - return new ClassificationConfig(numTopClasses); + return new ClassificationConfig(numTopClasses, topClassesResultsField); } public ClassificationConfig(Integer numTopClasses) { + this(numTopClasses, null); + } + + public ClassificationConfig(Integer numTopClasses, String topClassesResultsField) { this.numTopClasses = numTopClasses == null ? 0 : numTopClasses; + this.topClassesResultsField = topClassesResultsField == null ? DEFAULT_TOP_CLASSES_RESULT_FIELD : topClassesResultsField; } public ClassificationConfig(StreamInput in) throws IOException { this.numTopClasses = in.readInt(); + this.topClassesResultsField = in.readString(); } public int getNumTopClasses() { return numTopClasses; } + public String getTopClassesResultsField() { + return topClassesResultsField; + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeInt(numTopClasses); + out.writeString(topClassesResultsField); } @Override @@ -59,12 +74,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ClassificationConfig that = (ClassificationConfig) o; - return Objects.equals(numTopClasses, that.numTopClasses); + return Objects.equals(numTopClasses, that.numTopClasses) && Objects.equals(topClassesResultsField, that.topClassesResultsField); } @Override public int hashCode() { - return Objects.hash(numTopClasses); + return Objects.hash(numTopClasses, topClassesResultsField); } @Override @@ -73,6 +88,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (numTopClasses != 0) { builder.field(NUM_TOP_CLASSES.getPreferredName(), numTopClasses); } + builder.field(TOP_CLASSES_RESULT_FIELD.getPreferredName(), topClassesResultsField); builder.endObject(); return builder; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResultsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResultsTests.java index ba90fece02f2a..cc37e253806b0 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResultsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResultsTests.java @@ -8,6 +8,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.ingest.IngestDocument; import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ClassificationConfig; import java.util.Arrays; import java.util.Collections; @@ -37,7 +38,7 @@ private static ClassificationInferenceResults.TopClassEntry createRandomClassEnt public void testWriteResultsWithClassificationLabel() { ClassificationInferenceResults result = new ClassificationInferenceResults(1.0, "foo", Collections.emptyList()); IngestDocument document = new IngestDocument(new HashMap<>(), new HashMap<>()); - result.writeResult(document, "result_field"); + result.writeResult(document, "result_field", ClassificationConfig.EMPTY_PARAMS); assertThat(document.getFieldValue("result_field", String.class), equalTo("foo")); } @@ -45,7 +46,7 @@ public void testWriteResultsWithClassificationLabel() { public void testWriteResultsWithoutClassificationLabel() { ClassificationInferenceResults result = new ClassificationInferenceResults(1.0, null, Collections.emptyList()); IngestDocument document = new IngestDocument(new HashMap<>(), new HashMap<>()); - result.writeResult(document, "result_field"); + result.writeResult(document, "result_field", ClassificationConfig.EMPTY_PARAMS); assertThat(document.getFieldValue("result_field", String.class), equalTo("1.0")); } @@ -60,15 +61,17 @@ public void testWriteResultsWithTopClasses() { "foo", entries); IngestDocument document = new IngestDocument(new HashMap<>(), new HashMap<>()); - result.writeResult(document, "result_field"); + result.writeResult(document, "result_field", new ClassificationConfig(3, "bar")); - List list = document.getFieldValue("result_field", List.class); + List list = document.getFieldValue("bar", List.class); assertThat(list.size(), equalTo(3)); for(int i = 0; i < 3; i++) { Map map = (Map)list.get(i); assertThat(map, equalTo(entries.get(i).asValueMap())); } + + assertThat(document.getFieldValue("result_field", String.class), equalTo("foo")); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResultsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResultsTests.java index 4f2d5926c84dc..5c26fea0c06f5 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResultsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResultsTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.ingest.IngestDocument; import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.ml.inference.results.RegressionInferenceResults; +import org.elasticsearch.xpack.core.ml.inference.trainedmodel.RegressionConfig; import java.util.HashMap; @@ -24,7 +25,7 @@ public static RegressionInferenceResults createRandomResults() { public void testWriteResults() { RegressionInferenceResults result = new RegressionInferenceResults(0.3); IngestDocument document = new IngestDocument(new HashMap<>(), new HashMap<>()); - result.writeResult(document, "result_field"); + result.writeResult(document, "result_field", new RegressionConfig()); assertThat(document.getFieldValue("result_field", Double.class), equalTo(0.3)); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfigTests.java index 808aaf960f4e1..aff29d9bf8b74 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfigTests.java @@ -10,22 +10,27 @@ import org.elasticsearch.test.AbstractWireSerializingTestCase; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import static org.hamcrest.Matchers.equalTo; public class ClassificationConfigTests extends AbstractWireSerializingTestCase { public static ClassificationConfig randomClassificationConfig() { - return new ClassificationConfig(randomBoolean() ? null : randomIntBetween(-1, 10)); + return new ClassificationConfig(randomBoolean() ? null : randomIntBetween(-1, 10), + randomBoolean() ? null : randomAlphaOfLength(10)); } public void testFromMap() { - ClassificationConfig expected = new ClassificationConfig(0); + ClassificationConfig expected = ClassificationConfig.EMPTY_PARAMS; assertThat(ClassificationConfig.fromMap(Collections.emptyMap()), equalTo(expected)); - expected = new ClassificationConfig(3); - assertThat(ClassificationConfig.fromMap(Collections.singletonMap(ClassificationConfig.NUM_TOP_CLASSES.getPreferredName(), 3)), - equalTo(expected)); + expected = new ClassificationConfig(3, "foo"); + Map configMap = new HashMap<>(); + configMap.put(ClassificationConfig.NUM_TOP_CLASSES.getPreferredName(), 3); + configMap.put(ClassificationConfig.TOP_CLASSES_RESULT_FIELD.getPreferredName(), "foo"); + assertThat(ClassificationConfig.fromMap(configMap), equalTo(expected)); } public void testFromMapWithUnknownField() { diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java index c28c0a275d43c..315010282a4f4 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java @@ -147,20 +147,8 @@ public void testSimulate() { " {\n" + " \"inference\": {\n" + " \"target_field\": \"result_class\",\n" + - " \"inference_config\": {\"classification\":{}},\n" + - " \"model_id\": \"test_classification\",\n" + - " \"field_mappings\": {\n" + - " \"col1\": \"col1\",\n" + - " \"col2\": \"col2\",\n" + - " \"col3\": \"col3\",\n" + - " \"col4\": \"col4\"\n" + - " }\n" + - " }\n" + - " },\n" + - " {\n" + - " \"inference\": {\n" + - " \"target_field\": \"result_class_prob\",\n" + - " \"inference_config\": {\"classification\": {\"num_top_classes\":2}},\n" + + " \"inference_config\": {\"classification\": " + + " {\"num_top_classes\":2, \"top_classes_result_field\": \"result_class_prob\"}},\n" + " \"model_id\": \"test_classification\",\n" + " \"field_mappings\": {\n" + " \"col1\": \"col1\",\n" + diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java index a39588fbcfe58..0450cd4aa966b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java @@ -35,7 +35,6 @@ import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import org.elasticsearch.xpack.ml.notifications.InferenceAuditor; -import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -154,7 +153,7 @@ void mutateDocument(InternalInferModelAction.Response response, IngestDocument i if (response.getInferenceResults().isEmpty()) { throw new ElasticsearchStatusException("Unexpected empty inference response", RestStatus.INTERNAL_SERVER_ERROR); } - response.getInferenceResults().get(0).writeResult(ingestDocument, this.targetField); + response.getInferenceResults().get(0).writeResult(ingestDocument, this.targetField, inferenceConfig); if (includeModelMetadata) { ingestDocument.setFieldValue(modelInfoField + "." + MODEL_ID, modelId); } @@ -227,8 +226,7 @@ int numInferenceProcessors() { } @Override - public InferenceProcessor create(Map processorFactories, String tag, Map config) - throws Exception { + public InferenceProcessor create(Map processorFactories, String tag, Map config) { if (this.maxIngestProcessors <= currentInferenceProcessors) { throw new ElasticsearchStatusException("Max number of inference processors reached, total inference processors [{}]. " + @@ -267,7 +265,7 @@ void setMaxIngestProcessors(int maxIngestProcessors) { this.maxIngestProcessors = maxIngestProcessors; } - InferenceConfig inferenceConfigFromMap(Map inferenceConfig) throws IOException { + InferenceConfig inferenceConfigFromMap(Map inferenceConfig) { ExceptionsHelper.requireNonNull(inferenceConfig, INFERENCE_CONFIG); if (inferenceConfig.size() != 1) { @@ -284,7 +282,7 @@ InferenceConfig inferenceConfigFromMap(Map inferenceConfig) thro Map valueMap = (Map)value; if (inferenceConfig.containsKey(ClassificationConfig.NAME)) { - checkSupportedVersion(new ClassificationConfig(0)); + checkSupportedVersion(ClassificationConfig.EMPTY_PARAMS); return ClassificationConfig.fromMap(valueMap); } else if (inferenceConfig.containsKey(RegressionConfig.NAME)) { checkSupportedVersion(new RegressionConfig()); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java index fc8bbbb684aeb..b9132d5e2534f 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java @@ -51,7 +51,7 @@ public void testMutateDocumentWithClassification() { "my_processor", targetField, "classification_model", - new ClassificationConfig(0), + ClassificationConfig.EMPTY_PARAMS, Collections.emptyMap(), "ml.my_processor", true); @@ -78,7 +78,7 @@ public void testMutateDocumentClassificationTopNClasses() { "my_processor", targetField, "classification_model", - new ClassificationConfig(2), + new ClassificationConfig(2, null), Collections.emptyMap(), "ml.my_processor", true); @@ -96,10 +96,44 @@ public void testMutateDocumentClassificationTopNClasses() { true); inferenceProcessor.mutateDocument(response, document); - assertThat((List>)document.getFieldValue(targetField, List.class), + assertThat((List>)document.getFieldValue(ClassificationConfig.DEFAULT_TOP_CLASSES_RESULT_FIELD, List.class), contains(classes.stream().map(ClassificationInferenceResults.TopClassEntry::asValueMap).toArray(Map[]::new))); assertThat(document.getFieldValue("ml", Map.class), equalTo(Collections.singletonMap("my_processor", Collections.singletonMap("model_id", "classification_model")))); + assertThat(document.getFieldValue(targetField, String.class), equalTo("foo")); + } + + @SuppressWarnings("unchecked") + public void testMutateDocumentClassificationTopNClassesWithSpecificField() { + String targetField = "classification_value_probabilities"; + InferenceProcessor inferenceProcessor = new InferenceProcessor(client, + auditor, + "my_processor", + targetField, + "classification_model", + new ClassificationConfig(2, "my_top_classes"), + Collections.emptyMap(), + "ml.my_processor", + true); + + Map source = new HashMap<>(); + Map ingestMetadata = new HashMap<>(); + IngestDocument document = new IngestDocument(source, ingestMetadata); + + List classes = new ArrayList<>(2); + classes.add(new ClassificationInferenceResults.TopClassEntry("foo", 0.6)); + classes.add(new ClassificationInferenceResults.TopClassEntry("bar", 0.4)); + + InternalInferModelAction.Response response = new InternalInferModelAction.Response( + Collections.singletonList(new ClassificationInferenceResults(1.0, "foo", classes)), + true); + inferenceProcessor.mutateDocument(response, document); + + assertThat((List>)document.getFieldValue("my_top_classes", List.class), + contains(classes.stream().map(ClassificationInferenceResults.TopClassEntry::asValueMap).toArray(Map[]::new))); + assertThat(document.getFieldValue("ml", Map.class), + equalTo(Collections.singletonMap("my_processor", Collections.singletonMap("model_id", "classification_model")))); + assertThat(document.getFieldValue(targetField, String.class), equalTo("foo")); } public void testMutateDocumentRegression() { @@ -194,7 +228,7 @@ public void testGenerateRequestWithEmptyMapping() { "my_processor", "my_field", modelId, - new ClassificationConfig(topNClasses), + new ClassificationConfig(topNClasses, null), Collections.emptyMap(), "ml.my_processor", false); @@ -225,7 +259,7 @@ public void testGenerateWithMapping() { "my_processor", "my_field", modelId, - new ClassificationConfig(topNClasses), + new ClassificationConfig(topNClasses, null), fieldMapping, "ml.my_processor", false); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java index dee5253f5046c..66327d31c8178 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java @@ -131,7 +131,7 @@ public void testInferModels() throws Exception { // Test classification - request = new InternalInferModelAction.Request(modelId2, toInfer, new ClassificationConfig(0), true); + request = new InternalInferModelAction.Request(modelId2, toInfer, ClassificationConfig.EMPTY_PARAMS, true); response = client().execute(InternalInferModelAction.INSTANCE, request).actionGet(); assertThat(response.getInferenceResults() .stream() @@ -140,7 +140,7 @@ public void testInferModels() throws Exception { contains("not_to_be", "to_be")); // Get top classes - request = new InternalInferModelAction.Request(modelId2, toInfer, new ClassificationConfig(2), true); + request = new InternalInferModelAction.Request(modelId2, toInfer, new ClassificationConfig(2, null), true); response = client().execute(InternalInferModelAction.INSTANCE, request).actionGet(); ClassificationInferenceResults classificationInferenceResults = @@ -159,7 +159,7 @@ public void testInferModels() throws Exception { greaterThan(classificationInferenceResults.getTopClasses().get(1).getProbability())); // Test that top classes restrict the number returned - request = new InternalInferModelAction.Request(modelId2, toInfer2, new ClassificationConfig(1), true); + request = new InternalInferModelAction.Request(modelId2, toInfer2, new ClassificationConfig(1, null), true); response = client().execute(InternalInferModelAction.INSTANCE, request).actionGet(); classificationInferenceResults = (ClassificationInferenceResults)response.getInferenceResults().get(0); From 64b50443cb8cc39f0424e109f1c92a7d453791f2 Mon Sep 17 00:00:00 2001 From: Vishnu Chilamakuru Date: Mon, 9 Dec 2019 19:09:05 +0530 Subject: [PATCH 120/686] Add Validation for maxQueryTerms to be greater than 0 for MoreLikeThisQuery (#49966) Adds validation for maxQueryTerms to be greater than 0 for MoreLikeThisQuery and MoreLikeThisQueryBuilder. Closes #49927 --- .../common/lucene/search/MoreLikeThisQuery.java | 3 +++ .../index/query/MoreLikeThisQueryBuilder.java | 3 +++ .../search/morelikethis/MoreLikeThisQueryTests.java | 12 ++++++++++++ .../index/query/MoreLikeThisQueryBuilderTests.java | 12 +++++++++++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/lucene/search/MoreLikeThisQuery.java b/server/src/main/java/org/elasticsearch/common/lucene/search/MoreLikeThisQuery.java index 394b8bbe65d38..23764a18a3e1c 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/search/MoreLikeThisQuery.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/search/MoreLikeThisQuery.java @@ -308,6 +308,9 @@ public int getMaxQueryTerms() { } public void setMaxQueryTerms(int maxQueryTerms) { + if (maxQueryTerms <= 0) { + throw new IllegalArgumentException("requires 'maxQueryTerms' to be greater than 0"); + } this.maxQueryTerms = maxQueryTerms; } diff --git a/server/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilder.java index 597d81215c3a3..4e94b9e6327a7 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilder.java @@ -577,6 +577,9 @@ public Item[] unlikeItems() { * Defaults to {@code 25}. */ public MoreLikeThisQueryBuilder maxQueryTerms(int maxQueryTerms) { + if (maxQueryTerms <= 0) { + throw new IllegalArgumentException("requires 'maxQueryTerms' to be greater than 0"); + } this.maxQueryTerms = maxQueryTerms; return this; } diff --git a/server/src/test/java/org/elasticsearch/common/lucene/search/morelikethis/MoreLikeThisQueryTests.java b/server/src/test/java/org/elasticsearch/common/lucene/search/morelikethis/MoreLikeThisQueryTests.java index fb09ceb839c37..337c61243baa3 100644 --- a/server/src/test/java/org/elasticsearch/common/lucene/search/morelikethis/MoreLikeThisQueryTests.java +++ b/server/src/test/java/org/elasticsearch/common/lucene/search/morelikethis/MoreLikeThisQueryTests.java @@ -33,6 +33,7 @@ import org.elasticsearch.common.lucene.search.MoreLikeThisQuery; import org.elasticsearch.test.ESTestCase; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; public class MoreLikeThisQueryTests extends ESTestCase { @@ -64,4 +65,15 @@ public void testSimple() throws Exception { reader.close(); indexWriter.close(); } + + public void testValidateMaxQueryTerms() { + IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, + () -> new MoreLikeThisQuery("lucene", new String[]{"text"}, Lucene.STANDARD_ANALYZER).setMaxQueryTerms(0)); + assertThat(e1.getMessage(), containsString("requires 'maxQueryTerms' to be greater than 0")); + + IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, + () -> new MoreLikeThisQuery("lucene", new String[]{"text"}, Lucene.STANDARD_ANALYZER).setMaxQueryTerms(-3)); + assertThat(e2.getMessage(), containsString("requires 'maxQueryTerms' to be greater than 0")); + } + } diff --git a/server/src/test/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilderTests.java index 8099f9243466f..8a9ced02fb920 100644 --- a/server/src/test/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilderTests.java @@ -163,7 +163,7 @@ protected MoreLikeThisQueryBuilder doCreateTestQueryBuilder() { queryBuilder.unlike(randomUnlikeItems); } if (randomBoolean()) { - queryBuilder.maxQueryTerms(randomInt(25)); + queryBuilder.maxQueryTerms(randomIntBetween(1, 25)); } if (randomBoolean()) { queryBuilder.minTermFreq(randomInt(5)); @@ -340,6 +340,16 @@ public void testMoreLikeThisBuilder() throws Exception { assertThat(mltQuery.getMaxQueryTerms(), equalTo(12)); } + public void testValidateMaxQueryTerms() { + IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, + () -> new MoreLikeThisQueryBuilder(new String[]{"name.first", "name.last"}, new String[]{"something"}, null).maxQueryTerms(0)); + assertThat(e1.getMessage(), containsString("requires 'maxQueryTerms' to be greater than 0")); + + IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, + () -> new MoreLikeThisQueryBuilder(new String[]{"name.first", "name.last"}, new String[]{"something"}, null).maxQueryTerms(-3)); + assertThat(e2.getMessage(), containsString("requires 'maxQueryTerms' to be greater than 0")); + } + public void testItemSerialization() throws IOException { Item expectedItem = generateRandomItem(); BytesStreamOutput output = new BytesStreamOutput(); From fbe676acbcefd40f855e9965cd43120a407abd33 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 9 Dec 2019 08:39:17 -0500 Subject: [PATCH 121/686] [DOCS] Correct inline shape snippets in shape query docs (#49921) In the shape query docs, the index mapping snippet uses the "geometry" shape field mapping. However, the doc index snippet uses the "location" property. This changes the "location" property to "geometry". It also adds a comment containing the search result snippet. This should prevent similar issues in the future. --- docs/reference/query-dsl/shape-query.asciidoc | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/docs/reference/query-dsl/shape-query.asciidoc b/docs/reference/query-dsl/shape-query.asciidoc index 6a1c7380976f2..a9850c3bf6814 100644 --- a/docs/reference/query-dsl/shape-query.asciidoc +++ b/docs/reference/query-dsl/shape-query.asciidoc @@ -37,10 +37,10 @@ PUT /example } } -POST /example/_doc?refresh +PUT /example/_doc/1?refresh=wait_for { "name": "Lucky Landing", - "location": { + "geometry": { "type": "point", "coordinates": [1355.400544, 5255.530286] } @@ -69,6 +69,47 @@ GET /example/_search } -------------------------------------------------- +//// +[source,console-result] +-------------------------------------------------- +{ + "took": 3, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 0.0, + "hits": [ + { + "_index": "example", + "_id": "1", + "_score": 0.0, + "_source": { + "name": "Lucky Landing", + "geometry": { + "type": "point", + "coordinates": [ + 1355.400544, + 5255.530286 + ] + } + } + } + ] + } +} +-------------------------------------------------- +// TESTRESPONSE[s/"took": 3/"took": $body.took/] +//// + ==== Pre-Indexed Shape The Query also supports using a shape which has already been indexed in From b9f5fa15fb7da6d7da24a6ead67fbd2adf5795da Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Mon, 9 Dec 2019 08:50:27 -0500 Subject: [PATCH 122/686] [ML] Use query in cardinality check (#49939) When checking the cardinality of a field, the query should be take into account. The user might know about some bad data in their index and want to filter down to the target_field values they care about. --- .../ml/integration/ClassificationIT.java | 25 ++++++++++++++++++- ...NativeDataFrameAnalyticsIntegTestCase.java | 12 +++++++-- .../ExtractedFieldsDetectorFactory.java | 2 +- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java index 1d3f6bf4f2e75..f5db9ae690a96 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java @@ -15,6 +15,8 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; @@ -228,7 +230,7 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty_DependentVariableI assertEvaluation(BOOLEAN_FIELD, BOOLEAN_FIELD_VALUES, "ml.boolean-field_prediction"); } - public void testDependentVariableCardinalityTooHighError() { + public void testDependentVariableCardinalityTooHighError() throws Exception { initialize("cardinality_too_high"); indexData(sourceIndex, 6, 5, KEYWORD_FIELD); // Index one more document with a class different than the two already used. @@ -246,6 +248,27 @@ public void testDependentVariableCardinalityTooHighError() { assertThat(e.getMessage(), equalTo("Field [keyword-field] must have at most [2] distinct values but there were at least [3]")); } + public void testDependentVariableCardinalityTooHighButWithQueryMakesItWithinRange() throws Exception { + initialize("cardinality_too_high_with_query"); + indexData(sourceIndex, 6, 5, KEYWORD_FIELD); + // Index one more document with a class different than the two already used. + client().execute(IndexAction.INSTANCE, new IndexRequest(sourceIndex) + .source(KEYWORD_FIELD, "fox") + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)) + .actionGet(); + QueryBuilder query = QueryBuilders.boolQuery().filter(QueryBuilders.termsQuery(KEYWORD_FIELD, KEYWORD_FIELD_VALUES)); + + DataFrameAnalyticsConfig config = buildAnalytics(jobId, sourceIndex, destIndex, null, new Classification(KEYWORD_FIELD), query); + registerAnalytics(config); + putAnalytics(config); + + // Should not throw + startAnalytics(jobId); + waitUntilAnalyticsIsStopped(jobId); + + assertProgress(jobId, 100, 100, 100, 100); + } + private void initialize(String jobId) { this.jobId = jobId; this.sourceIndex = jobId + "_source_index"; diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java index 0b9e2c19961d8..29ef54d3f7524 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java @@ -16,6 +16,7 @@ import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.xpack.core.ml.action.DeleteDataFrameAnalyticsAction; @@ -37,6 +38,7 @@ import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; import org.elasticsearch.xpack.core.ml.notifications.AuditorField; import org.elasticsearch.xpack.core.ml.utils.PhaseProgress; +import org.elasticsearch.xpack.core.ml.utils.QueryProvider; import org.elasticsearch.xpack.ml.dataframe.DataFrameAnalyticsTask; import org.hamcrest.Matcher; import org.hamcrest.Matchers; @@ -161,10 +163,16 @@ protected EvaluateDataFrameAction.Response evaluateDataFrame(String index, Evalu } protected static DataFrameAnalyticsConfig buildAnalytics(String id, String sourceIndex, String destIndex, - @Nullable String resultsField, DataFrameAnalysis analysis) { + @Nullable String resultsField, DataFrameAnalysis analysis) throws Exception { + return buildAnalytics(id, sourceIndex, destIndex, resultsField, analysis, QueryBuilders.matchAllQuery()); + } + + protected static DataFrameAnalyticsConfig buildAnalytics(String id, String sourceIndex, String destIndex, + @Nullable String resultsField, DataFrameAnalysis analysis, + QueryBuilder queryBuilder) throws Exception { return new DataFrameAnalyticsConfig.Builder() .setId(id) - .setSource(new DataFrameAnalyticsSource(new String[] { sourceIndex }, null, null)) + .setSource(new DataFrameAnalyticsSource(new String[] { sourceIndex }, QueryProvider.fromParsedQuery(queryBuilder), null)) .setDest(new DataFrameAnalyticsDest(destIndex, resultsField)) .setAnalysis(analysis) .build(); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java index ea37bdf393aeb..8e6ad7a614b09 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java @@ -109,7 +109,7 @@ private void getCardinalitiesForFieldsWithLimit(String[] index, DataFrameAnalyti listener::onFailure ); - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().size(0); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().size(0).query(config.getSource().getParsedQuery()); for (Map.Entry entry : fieldCardinalityLimits.entrySet()) { String fieldName = entry.getKey(); Long limit = entry.getValue(); From 79befab477aa3b9500ebfc998d5be0db5b2b64a1 Mon Sep 17 00:00:00 2001 From: Marios Trivyzas Date: Mon, 9 Dec 2019 15:34:23 +0100 Subject: [PATCH 123/686] SQL: [Tests] Unmute Pivot from NodeSublassTests (#49925) The `testReplaceChildren()` has been fixed for Pivot as part of #49693. Reverting: #49045 --- .../elasticsearch/xpack/sql/tree/NodeSubclassTests.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/tree/NodeSubclassTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/tree/NodeSubclassTests.java index f27c65fbc7c56..d3a4870c24857 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/tree/NodeSubclassTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/tree/NodeSubclassTests.java @@ -28,19 +28,17 @@ import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; import org.elasticsearch.xpack.sql.expression.gen.processor.ConstantProcessor; import org.elasticsearch.xpack.sql.expression.gen.processor.Processor; -import org.elasticsearch.xpack.sql.expression.predicate.conditional.Iif; import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfConditional; import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfNull; +import org.elasticsearch.xpack.sql.expression.predicate.conditional.Iif; import org.elasticsearch.xpack.sql.expression.predicate.fulltext.FullTextPredicate; import org.elasticsearch.xpack.sql.expression.predicate.operator.comparison.In; import org.elasticsearch.xpack.sql.expression.predicate.operator.comparison.InPipe; import org.elasticsearch.xpack.sql.expression.predicate.regex.Like; import org.elasticsearch.xpack.sql.expression.predicate.regex.LikePattern; -import org.elasticsearch.xpack.sql.plan.logical.Pivot; import org.elasticsearch.xpack.sql.tree.NodeTests.ChildrenAreAProperty; import org.elasticsearch.xpack.sql.tree.NodeTests.Dummy; import org.elasticsearch.xpack.sql.tree.NodeTests.NoChildren; -import org.junit.Assume; import org.mockito.exceptions.base.MockitoException; import java.io.IOException; @@ -166,9 +164,6 @@ public void testTransform() throws Exception { * Test {@link Node#replaceChildren} implementation on {@link #subclass}. */ public void testReplaceChildren() throws Exception { - // TODO: Provide a proper fix for: https://github.com/elastic/elasticsearch/issues/48900 - Assume.assumeFalse(subclass.equals(Pivot.class)); - Constructor ctor = longestCtor(subclass); Object[] nodeCtorArgs = ctorArgs(ctor); T node = ctor.newInstance(nodeCtorArgs); From e716e61cbde963d678c66b25a8ce59facf447b82 Mon Sep 17 00:00:00 2001 From: sabi0 <2sabio@gmail.com> Date: Mon, 9 Dec 2019 16:16:45 +0100 Subject: [PATCH 124/686] [DOCS] Fix description typo and correct capitalization for IngestGeoIpPlugin docs (#49241) --- modules/ingest-geoip/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ingest-geoip/build.gradle b/modules/ingest-geoip/build.gradle index 6b49c26c41570..ce9dc7d574c12 100644 --- a/modules/ingest-geoip/build.gradle +++ b/modules/ingest-geoip/build.gradle @@ -20,7 +20,7 @@ import org.apache.tools.ant.taskdefs.condition.Os esplugin { - description 'Ingest processor that uses looksup geo data based on ip adresses using the Maxmind geo database' + description 'Ingest processor that uses lookup geo data based on IP adresses using the MaxMind geo database' classname 'org.elasticsearch.ingest.geoip.IngestGeoIpPlugin' } From 5aeff9357cb6e5c870a0afcff0083ddadb876de7 Mon Sep 17 00:00:00 2001 From: Artur Carvalho Date: Mon, 9 Dec 2019 16:24:03 +0100 Subject: [PATCH 125/686] [Docs] Fix typo in getting-started.asciidoc (#49985) --- docs/reference/getting-started.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/getting-started.asciidoc b/docs/reference/getting-started.asciidoc index ad67dcc0e6eb8..3de052001cbee 100755 --- a/docs/reference/getting-started.asciidoc +++ b/docs/reference/getting-started.asciidoc @@ -283,7 +283,7 @@ If you have a lot of documents to index, you can submit them in batches with the {ref}/docs-bulk.html[bulk API]. Using bulk to batch document operations is significantly faster than submitting requests individually as it minimizes network roundtrips. -The optimal batch size depends a number of factors: the document size and complexity, the indexing and search load, and the resources available to your cluster. A good place to start is with batches of 1,000 to 5,000 documents +The optimal batch size depends on a number of factors: the document size and complexity, the indexing and search load, and the resources available to your cluster. A good place to start is with batches of 1,000 to 5,000 documents and a total payload between 5MB and 15MB. From there, you can experiment to find the sweet spot. From b69e1d6a97163c96fa46f116240cb320828a3c58 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 9 Dec 2019 11:04:20 -0500 Subject: [PATCH 126/686] [DOCS] Correct `for in` example in Painless docs (#49991) Adds a needed `def` keyword to the `for in` example in the Painless docs. --- docs/painless/painless-lang-spec/painless-statements.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/painless/painless-lang-spec/painless-statements.asciidoc b/docs/painless/painless-lang-spec/painless-statements.asciidoc index b9aceed9cf097..ece2686838149 100644 --- a/docs/painless/painless-lang-spec/painless-statements.asciidoc +++ b/docs/painless/painless-lang-spec/painless-statements.asciidoc @@ -25,7 +25,7 @@ Painless also supports the `for in` syntax from Groovy: [source,painless] --------------------------------------------------------- -for (item : list) { +for (def item : list) { ... } --------------------------------------------------------- From a29fe9579465736bbb1a3066244e33ada8cd205e Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 9 Dec 2019 12:39:26 -0500 Subject: [PATCH 127/686] Enable dependent settings values to be validated (#49942) Today settings can declare dependencies on another setting. This declaration is implemented so that if the declared setting is not set when the declaring setting is, settings validation fails. Yet, in some cases we want not only that the setting is set, but that it also has a specific value. For example, with the monitoring exporter settings, if xpack.monitoring.exporters.my_exporter.host is set, we not only want that xpack.monitoring.exporters.my_exporter.type is set, but that it is also set to local. This commit extends the settings infrastructure so that this declaration is possible. The use of this in the monitoring exporter settings will be implemented in a follow-up. --- .../azure/AzureStorageSettings.java | 21 +++--- .../settings/AbstractScopedSettings.java | 12 ++-- .../common/settings/Setting.java | 65 ++++++++++++++++--- .../transport/RemoteClusterService.java | 6 +- .../transport/SniffConnectionStrategy.java | 2 +- .../common/settings/ScopedSettingsTests.java | 55 ++++++++++++++-- .../indices/settings/UpdateSettingsIT.java | 4 +- .../core/security/authc/RealmSettings.java | 4 +- 8 files changed, 137 insertions(+), 32 deletions(-) diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageSettings.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageSettings.java index 5162f1a223699..380eef87c4172 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageSettings.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageSettings.java @@ -62,29 +62,34 @@ final class AzureStorageSettings { public static final AffixSetting MAX_RETRIES_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "max_retries", (key) -> Setting.intSetting(key, RetryPolicy.DEFAULT_CLIENT_RETRY_COUNT, Setting.Property.NodeScope), - ACCOUNT_SETTING, KEY_SETTING); + () -> ACCOUNT_SETTING, () -> KEY_SETTING); /** * Azure endpoint suffix. Default to core.windows.net (CloudStorageAccount.DEFAULT_DNS). */ public static final AffixSetting ENDPOINT_SUFFIX_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "endpoint_suffix", - key -> Setting.simpleString(key, Property.NodeScope), ACCOUNT_SETTING, KEY_SETTING); + key -> Setting.simpleString(key, Property.NodeScope), () -> ACCOUNT_SETTING, () -> KEY_SETTING); public static final AffixSetting TIMEOUT_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "timeout", - (key) -> Setting.timeSetting(key, TimeValue.timeValueMinutes(-1), Property.NodeScope), ACCOUNT_SETTING, KEY_SETTING); + (key) -> Setting.timeSetting(key, TimeValue.timeValueMinutes(-1), Property.NodeScope), () -> ACCOUNT_SETTING, () -> KEY_SETTING); /** The type of the proxy to connect to azure through. Can be direct (no proxy, default), http or socks */ public static final AffixSetting PROXY_TYPE_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "proxy.type", (key) -> new Setting<>(key, "direct", s -> Proxy.Type.valueOf(s.toUpperCase(Locale.ROOT)), Property.NodeScope) - , ACCOUNT_SETTING, KEY_SETTING); + , () -> ACCOUNT_SETTING, () -> KEY_SETTING); /** The host name of a proxy to connect to azure through. */ public static final AffixSetting PROXY_HOST_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "proxy.host", - (key) -> Setting.simpleString(key, Property.NodeScope), KEY_SETTING, ACCOUNT_SETTING, PROXY_TYPE_SETTING); + (key) -> Setting.simpleString(key, Property.NodeScope), () -> KEY_SETTING, () -> ACCOUNT_SETTING, () -> PROXY_TYPE_SETTING); /** The port of a proxy to connect to azure through. */ - public static final Setting PROXY_PORT_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "proxy.port", - (key) -> Setting.intSetting(key, 0, 0, 65535, Setting.Property.NodeScope), ACCOUNT_SETTING, KEY_SETTING, PROXY_TYPE_SETTING, - PROXY_HOST_SETTING); + public static final Setting PROXY_PORT_SETTING = Setting.affixKeySetting( + AZURE_CLIENT_PREFIX_KEY, + "proxy.port", + (key) -> Setting.intSetting(key, 0, 0, 65535, Setting.Property.NodeScope), + () -> ACCOUNT_SETTING, + () -> KEY_SETTING, + () -> PROXY_TYPE_SETTING, + () -> PROXY_HOST_SETTING); private final String account; private final String connectString; diff --git a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java index 1cbdc29bb1654..81b3b92844d6b 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java @@ -530,20 +530,24 @@ void validate( } throw new IllegalArgumentException(msg); } else { - Set> settingsDependencies = setting.getSettingsDependencies(key); + Set settingsDependencies = setting.getSettingsDependencies(key); if (setting.hasComplexMatcher()) { setting = setting.getConcreteSetting(key); } if (validateDependencies && settingsDependencies.isEmpty() == false) { - for (final Setting settingDependency : settingsDependencies) { - if (settingDependency.existsOrFallbackExists(settings) == false) { + for (final Setting.SettingDependency settingDependency : settingsDependencies) { + final Setting dependency = settingDependency.getSetting(); + // validate the dependent setting is set + if (dependency.existsOrFallbackExists(settings) == false) { final String message = String.format( Locale.ROOT, "missing required setting [%s] for setting [%s]", - settingDependency.getKey(), + dependency.getKey(), setting.getKey()); throw new IllegalArgumentException(message); } + // validate the dependent setting value + settingDependency.validate(setting.getKey(), setting.get(settings), dependency.get(settings)); } } // the only time that validateInternalOrPrivateIndex should be true is if this call is coming via the update settings API diff --git a/server/src/main/java/org/elasticsearch/common/settings/Setting.java b/server/src/main/java/org/elasticsearch/common/settings/Setting.java index 2e6bfdc635cd2..bbfe56ffcda97 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/Setting.java +++ b/server/src/main/java/org/elasticsearch/common/settings/Setting.java @@ -564,11 +564,37 @@ public Setting getConcreteSetting(String key) { return this; } + /** + * Allows a setting to declare a dependency on another setting being set. Optionally, a setting can validate the value of the dependent + * setting. + */ + public interface SettingDependency { + + /** + * The setting to declare a dependency on. + * + * @return the setting + */ + Setting getSetting(); + + /** + * Validates the dependent setting value. + * + * @param key the key for this setting + * @param value the value of this setting + * @param dependency the value of the dependent setting + */ + default void validate(String key, Object value, Object dependency) { + + } + + } + /** * Returns a set of settings that are required at validation time. Unless all of the dependencies are present in the settings * object validation of setting must fail. */ - public Set> getSettingsDependencies(String key) { + public Set getSettingsDependencies(final String key) { return Collections.emptySet(); } @@ -671,13 +697,23 @@ public String toString() { }; } + /** + * Allows an affix setting to declare a dependency on another affix setting. + */ + public interface AffixSettingDependency extends SettingDependency { + + @Override + AffixSetting getSetting(); + + } + public static class AffixSetting extends Setting { private final AffixKey key; private final BiFunction> delegateFactory; - private final Set dependencies; + private final Set dependencies; public AffixSetting(AffixKey key, Setting delegate, BiFunction> delegateFactory, - AffixSetting... dependencies) { + AffixSettingDependency... dependencies) { super(key, delegate.defaultValue, delegate.parser, delegate.properties.toArray(new Property[0])); this.key = key; this.delegateFactory = delegateFactory; @@ -693,12 +729,25 @@ private Stream matchStream(Settings settings) { } @Override - public Set> getSettingsDependencies(String settingsKey) { + public Set getSettingsDependencies(String settingsKey) { if (dependencies.isEmpty()) { return Collections.emptySet(); } else { String namespace = key.getNamespace(settingsKey); - return dependencies.stream().map(s -> (Setting)s.getConcreteSettingForNamespace(namespace)).collect(Collectors.toSet()); + return dependencies.stream() + .map(s -> + new SettingDependency() { + @Override + public Setting getSetting() { + return s.getSetting().getConcreteSettingForNamespace(namespace); + } + + @Override + public void validate(final String key, final Object value, final Object dependency) { + s.validate(key, value, dependency); + }; + }) + .collect(Collectors.toSet()); } } @@ -1635,19 +1684,19 @@ public static AffixSetting prefixKeySetting(String prefix, Function AffixSetting affixKeySetting(String prefix, String suffix, Function> delegateFactory, - AffixSetting... dependencies) { + AffixSettingDependency... dependencies) { BiFunction> delegateFactoryWithNamespace = (ns, k) -> delegateFactory.apply(k); return affixKeySetting(new AffixKey(prefix, suffix), delegateFactoryWithNamespace, dependencies); } public static AffixSetting affixKeySetting(String prefix, String suffix, BiFunction> delegateFactory, - AffixSetting... dependencies) { + AffixSettingDependency... dependencies) { Setting delegate = delegateFactory.apply("_na_", "_na_"); return new AffixSetting<>(new AffixKey(prefix, suffix), delegate, delegateFactory, dependencies); } private static AffixSetting affixKeySetting(AffixKey key, BiFunction> delegateFactory, - AffixSetting... dependencies) { + AffixSettingDependency... dependencies) { Setting delegate = delegateFactory.apply("_na_", "_na_"); return new AffixSetting<>(key, delegate, delegateFactory, dependencies); } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index f398100049a8f..779d4deacd45a 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -100,19 +100,19 @@ public final class RemoteClusterService extends RemoteClusterAware implements Cl false, Setting.Property.Dynamic, Setting.Property.NodeScope), - SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS); + () -> SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS); public static final Setting.AffixSetting REMOTE_CLUSTER_PING_SCHEDULE = Setting.affixKeySetting( "cluster.remote.", "transport.ping_schedule", key -> timeSetting(key, TransportSettings.PING_SCHEDULE, Setting.Property.Dynamic, Setting.Property.NodeScope), - SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS); + () -> SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS); public static final Setting.AffixSetting REMOTE_CLUSTER_COMPRESS = Setting.affixKeySetting( "cluster.remote.", "transport.compress", key -> boolSetting(key, TransportSettings.TRANSPORT_COMPRESS, Setting.Property.Dynamic, Setting.Property.NodeScope), - SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS); + () -> SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS); private final TransportService transportService; private final Map remoteClusters = ConcurrentCollections.newConcurrentMap(); diff --git a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java index 9ec0f4afe9997..cfed1d01c47e3 100644 --- a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java @@ -116,7 +116,7 @@ public class SniffConnectionStrategy extends RemoteConnectionStrategy { }), Setting.Property.Dynamic, Setting.Property.NodeScope), - REMOTE_CLUSTER_SEEDS); + () -> REMOTE_CLUSTER_SEEDS); /** * The maximum number of connections that will be established to a remote cluster. For instance if there is only a single diff --git a/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java b/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java index 6c0f6b0751a65..f653ddd59b7f8 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java @@ -179,9 +179,9 @@ public void testDependentSettings() { Setting.AffixSetting stringSetting = Setting.affixKeySetting("foo.", "name", (k) -> Setting.simpleString(k, Property.Dynamic, Property.NodeScope)); Setting.AffixSetting intSetting = Setting.affixKeySetting("foo.", "bar", - (k) -> Setting.intSetting(k, 1, Property.Dynamic, Property.NodeScope), stringSetting); + (k) -> Setting.intSetting(k, 1, Property.Dynamic, Property.NodeScope), () -> stringSetting); - AbstractScopedSettings service = new ClusterSettings(Settings.EMPTY,new HashSet<>(Arrays.asList(intSetting, stringSetting))); + AbstractScopedSettings service = new ClusterSettings(Settings.EMPTY, new HashSet<>(Arrays.asList(intSetting, stringSetting))); IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> service.validate(Settings.builder().put("foo.test.bar", 7).build(), true)); @@ -195,6 +195,50 @@ public void testDependentSettings() { service.validate(Settings.builder().put("foo.test.bar", 7).build(), false); } + public void testDependentSettingsValidate() { + Setting.AffixSetting stringSetting = Setting.affixKeySetting( + "foo.", + "name", + (k) -> Setting.simpleString(k, Property.Dynamic, Property.NodeScope)); + Setting.AffixSetting intSetting = Setting.affixKeySetting( + "foo.", + "bar", + (k) -> Setting.intSetting(k, 1, Property.Dynamic, Property.NodeScope), + new Setting.AffixSettingDependency() { + + @Override + public Setting.AffixSetting getSetting() { + return stringSetting; + } + + @Override + public void validate(final String key, final Object value, final Object dependency) { + if ("valid".equals(dependency) == false) { + throw new SettingsException("[" + key + "] is set but [name] is [" + dependency + "]"); + } + } + }); + + AbstractScopedSettings service = new ClusterSettings(Settings.EMPTY, new HashSet<>(Arrays.asList(intSetting, stringSetting))); + + SettingsException iae = expectThrows( + SettingsException.class, + () -> service.validate(Settings.builder().put("foo.test.bar", 7).put("foo.test.name", "invalid").build(), true)); + assertEquals("[foo.test.bar] is set but [name] is [invalid]", iae.getMessage()); + + service.validate(Settings.builder() + .put("foo.test.bar", 7) + .put("foo.test.name", "valid") + .build(), + true); + + service.validate(Settings.builder() + .put("foo.test.bar", 7) + .put("foo.test.name", "invalid") + .build(), + false); + } + public void testDependentSettingsWithFallback() { Setting.AffixSetting nameFallbackSetting = Setting.affixKeySetting("fallback.", "name", k -> Setting.simpleString(k, Property.Dynamic, Property.NodeScope)); @@ -208,8 +252,11 @@ public void testDependentSettingsWithFallback() { : nameFallbackSetting.getConcreteSetting(k.replaceAll("^foo", "fallback")), Property.Dynamic, Property.NodeScope)); - Setting.AffixSetting barSetting = - Setting.affixKeySetting("foo.", "bar", k -> Setting.intSetting(k, 1, Property.Dynamic, Property.NodeScope), nameSetting); + Setting.AffixSetting barSetting = Setting.affixKeySetting( + "foo.", + "bar", + k -> Setting.intSetting(k, 1, Property.Dynamic, Property.NodeScope), + () -> nameSetting); final AbstractScopedSettings service = new ClusterSettings(Settings.EMPTY,new HashSet<>(Arrays.asList(nameFallbackSetting, nameSetting, barSetting))); diff --git a/server/src/test/java/org/elasticsearch/indices/settings/UpdateSettingsIT.java b/server/src/test/java/org/elasticsearch/indices/settings/UpdateSettingsIT.java index e897c33ffd569..3d063ac3690bb 100644 --- a/server/src/test/java/org/elasticsearch/indices/settings/UpdateSettingsIT.java +++ b/server/src/test/java/org/elasticsearch/indices/settings/UpdateSettingsIT.java @@ -93,12 +93,12 @@ public static class DummySettingPlugin extends Plugin { public static final Setting.AffixSetting DUMMY_ACCOUNT_USER = Setting.affixKeySetting("index.acc.", "user", k -> Setting.simpleString(k, Setting.Property.IndexScope, Setting.Property.Dynamic)); public static final Setting DUMMY_ACCOUNT_PW = Setting.affixKeySetting("index.acc.", "pw", - k -> Setting.simpleString(k, Setting.Property.IndexScope, Setting.Property.Dynamic), DUMMY_ACCOUNT_USER); + k -> Setting.simpleString(k, Setting.Property.IndexScope, Setting.Property.Dynamic), () -> DUMMY_ACCOUNT_USER); public static final Setting.AffixSetting DUMMY_ACCOUNT_USER_CLUSTER = Setting.affixKeySetting("cluster.acc.", "user", k -> Setting.simpleString(k, Setting.Property.NodeScope, Setting.Property.Dynamic)); public static final Setting DUMMY_ACCOUNT_PW_CLUSTER = Setting.affixKeySetting("cluster.acc.", "pw", - k -> Setting.simpleString(k, Setting.Property.NodeScope, Setting.Property.Dynamic), DUMMY_ACCOUNT_USER_CLUSTER); + k -> Setting.simpleString(k, Setting.Property.NodeScope, Setting.Property.Dynamic), () -> DUMMY_ACCOUNT_USER_CLUSTER); @Override public void onIndexModule(IndexModule indexModule) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java index fda2cf614c8bd..8b4b7fa8848b2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java @@ -20,7 +20,7 @@ /** * Provides a number of utility methods for interacting with {@link Settings} and {@link Setting} inside a {@link Realm}. - * Settings for realms use an {@link Setting#affixKeySetting(String, String, Function, Setting.AffixSetting[]) affix} style, + * Settings for realms use an {@link Setting#affixKeySetting(String, String, Function, Setting.AffixSettingDependency[]) affix} style, * where the type of the realm is part of the prefix, and name of the realm is the variable portion * (That is to set the order in a file realm named "file1", then full setting key would be * {@code xpack.security.authc.realms.file.file1.order}. @@ -74,7 +74,7 @@ public static Setting.AffixSetting secureString(String realmType, * The {@code Function} takes the realm-type as an argument. * @param suffix The suffix of the setting (everything following the realm name in the affix setting) * @param delegateFactory A factory to produce the concrete setting. - * See {@link Setting#affixKeySetting(String, String, Function, Setting.AffixSetting[])} + * See {@link Setting#affixKeySetting(String, String, Function, Setting.AffixSettingDependency[])} */ public static Function> affixSetting(String suffix, Function> delegateFactory) { return realmType -> Setting.affixKeySetting(realmSettingPrefix(realmType), suffix, delegateFactory); From 64bfd27d6874a0ea611a95f28b8f3d3b156cbafb Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Mon, 9 Dec 2019 09:57:29 -0800 Subject: [PATCH 128/686] Remove leftover debug log message (#49957) This was leftover from debugging #49204. --- .../src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index 85e58cf5ff3c7..05ea77720e123 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -257,7 +257,6 @@ class BuildPlugin implements Plugin { rootProject.gradle.taskGraph.whenReady { TaskExecutionGraph taskGraph -> List messages = [] Map> requiredJavaVersions = (Map>) extraProperties.get('requiredJavaVersions') - task.logger.warn(requiredJavaVersions.toString()) for (Map.Entry> entry : requiredJavaVersions) { if (BuildParams.javaVersions.any { it.version == entry.key }) { continue From 0033ac104c3a5390dc821747ee0c05277142329a Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Mon, 9 Dec 2019 09:58:44 -0800 Subject: [PATCH 129/686] Make testclusters registry extension name unique (#49956) The testclusters registory is a singleton extension element added to the root project which tracks which test clusters are used throughout the multi project. But having the same name as the extension used to configure test clusters within each subprojects breaks using a single project for an external plugin. This commit renames the registry extension to make it unique. closes #49787 --- .../gradle/testclusters/TestClustersPlugin.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersPlugin.java index da10235a9d9a2..a96f8913e8208 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersPlugin.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersPlugin.java @@ -38,6 +38,7 @@ public class TestClustersPlugin implements Plugin { private static final String LIST_TASK_NAME = "listTestClusters"; public static final String EXTENSION_NAME = "testClusters"; + private static final String REGISTRY_EXTENSION_NAME = "testClustersRegistry"; private static final Logger logger = Logging.getLogger(TestClustersPlugin.class); @@ -56,9 +57,9 @@ public void apply(Project project) { // provide a task to be able to list defined clusters. createListClustersTask(project, container); - if (project.getRootProject().getExtensions().findByName("testClusters") == null) { + if (project.getRootProject().getExtensions().findByName(REGISTRY_EXTENSION_NAME) == null) { TestClustersRegistry registry = project.getRootProject().getExtensions() - .create("testClusters", TestClustersRegistry.class); + .create(REGISTRY_EXTENSION_NAME, TestClustersRegistry.class); // When we know what tasks will run, we claim the clusters of those task to differentiate between clusters // that are defined in the build script and the ones that will actually be used in this invocation of gradle From 2c353ff81b75c98d23c85c2e05b44445f5868692 Mon Sep 17 00:00:00 2001 From: shiwenjie12 <656336863@qq.com> Date: Tue, 10 Dec 2019 02:02:55 +0800 Subject: [PATCH 130/686] Modify notes (#48331) Modify notes --- .../action/search/SearchExecutionStatsCollector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchExecutionStatsCollector.java b/server/src/main/java/org/elasticsearch/action/search/SearchExecutionStatsCollector.java index 0ffad5aa4065b..b284a2204a767 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchExecutionStatsCollector.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchExecutionStatsCollector.java @@ -31,7 +31,7 @@ /** * A wrapper of search action listeners (search results) that unwraps the query * result to get the piggybacked queue size and service time EWMA, adding those - * values to the coordinating nodes' {@code ResponseCollectorService}. + * values to the coordinating nodes' {@link ResponseCollectorService}. */ public final class SearchExecutionStatsCollector implements ActionListener { From 0fe731787bf5a6eb2686f9a902ce6fffad8b25f2 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 9 Dec 2019 13:16:16 -0500 Subject: [PATCH 131/686] [DOCS] Skip synced flush docs tests (#49986) The current snippets in the synced flush docs can cause conflicts with other background syncs, such as the global checkpoint sync or retention lease sync, in the docs tests. This skips tests for those snippets to avoid conflicts. --- docs/reference/indices/synced-flush.asciidoc | 26 +++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/reference/indices/synced-flush.asciidoc b/docs/reference/indices/synced-flush.asciidoc index 99c5dd4beeadd..cb2c40793091a 100644 --- a/docs/reference/indices/synced-flush.asciidoc +++ b/docs/reference/indices/synced-flush.asciidoc @@ -10,7 +10,7 @@ Performs a synced flush on one or more indices. -------------------------------------------------- POST /twitter/_flush/synced -------------------------------------------------- -// TEST[setup:twitter] +// TEST[skip: Synced flush can conflict with scheduled flushes in doc tests] [[synced-flush-api-request]] @@ -80,7 +80,7 @@ section of the shard stats returned by the <> API: -------------------------------------------------- GET /twitter/_stats?filter_path=**.commit&level=shards <1> -------------------------------------------------- -// TEST[s/^/PUT twitter\nPOST twitter\/_flush\/synced\n/] +// TEST[skip: Synced flush can conflict with scheduled flushes in doc tests] <1> `filter_path` is used to reduce the verbosity of the response, but is entirely optional @@ -116,10 +116,7 @@ The API returns the following response: } } -------------------------------------------------- -// TESTRESPONSE[s/"id" : "3M3zkw2GHMo2Y4h4\/KFKCg=="/"id": $body.indices.twitter.shards.0.0.commit.id/] -// TESTRESPONSE[s/"translog_uuid" : "hnOG3xFcTDeoI_kvvvOdNA"/"translog_uuid": $body.indices.twitter.shards.0.0.commit.user_data.translog_uuid/] -// TESTRESPONSE[s/"history_uuid" : "XP7KDJGiS1a2fHYiFL5TXQ"/"history_uuid": $body.indices.twitter.shards.0.0.commit.user_data.history_uuid/] -// TESTRESPONSE[s/"sync_id" : "AVvFY-071siAOuFGEO9P"/"sync_id": $body.indices.twitter.shards.0.0.commit.user_data.sync_id/] +// TEST[skip: Synced flush can conflict with scheduled flushes in doc tests] <1> the `sync id` marker NOTE: The `sync_id` marker is removed as soon as the shard is flushed again, and @@ -172,7 +169,7 @@ A replica shard failed to sync-flush. ---- POST /kimchy/_flush/synced ---- -// TEST[s/^/PUT kimchy\n/] +// TEST[skip: Synced flush can conflict with scheduled flushes in doc tests] [[synced-flush-api-multi-ex]] @@ -182,8 +179,7 @@ POST /kimchy/_flush/synced -------------------------------------------------- POST /kimchy,elasticsearch/_flush/synced -------------------------------------------------- -// TEST[s/^/PUT elasticsearch\n/] -// TEST[continued] +// TEST[skip: Synced flush can conflict with scheduled flushes in doc tests] [[synced-flush-api-all-ex]] @@ -193,7 +189,7 @@ POST /kimchy,elasticsearch/_flush/synced -------------------------------------------------- POST /_flush/synced -------------------------------------------------- -// TEST[setup:twitter] +// TEST[skip: Synced flush can conflict with scheduled flushes in doc tests] The response contains details about how many shards were successfully sync-flushed and information about any failure. @@ -217,12 +213,12 @@ successfully sync-flushed: } } -------------------------------------------------- -// TESTRESPONSE[s/"successful": 2/"successful": 1/] +// TEST[skip: Synced flush can conflict with scheduled flushes in doc tests] The following response indicates one shard group failed due to pending operations: -[source,js] +[source,console-result] -------------------------------------------------- { "_shards": { @@ -243,13 +239,13 @@ due to pending operations: } } -------------------------------------------------- -// NOTCONSOLE +// TEST[skip: Synced flush can conflict with scheduled flushes in doc tests] Sometimes the failures are specific to a shard replica. The copies that failed will not be eligible for fast recovery but those that succeeded still will be. This case is reported as follows: -[source,js] +[source,console-result] -------------------------------------------------- { "_shards": { @@ -278,4 +274,4 @@ This case is reported as follows: } } -------------------------------------------------- -// NOTCONSOLE +// TEST[skip: Synced flush can conflict with scheduled flushes in doc tests] From adf64a2af919a54b45564856d06af9e8cb55fcb2 Mon Sep 17 00:00:00 2001 From: William Brafford Date: Mon, 9 Dec 2019 16:17:00 -0500 Subject: [PATCH 132/686] Refactor utility code in qa:os: tests (#49945) This refactor bridges some gaps between a long-running feature branch (#49268) and the master branch. First of all, this PR gives our PackagingTestCase class some methods to start and stop Elasticsearch that will switch on packaging type and delegate to the appropriate utility class for deb/RPM packages, archive installations, and Docker. These methods should be very useful as we continue group tests by function rather than by package or platform type. Second, the password-protected keystore tests have a particular need to read the output of Elasticsearch startup commands. In order to make this easer to do, some commands now return Shell.Result objects so that tests can check over output to the shell. To that end, there's also an assertElasticsearchFailure method that will handle checking for startup failures for the various distribution types. There is an update to the Powershell startup script for archives that asynchronously redirects the output of the Powershell process to files that we can read for errors. Finally, we use the ES_STARTUP_SLEEP_TIME environment variable to make sure that our startup commands wait long enough before exiting for errors to make it to the standard output and error streams. --- .../packaging/test/ArchiveTests.java | 30 ++-- .../packaging/test/PackageTests.java | 53 ++++--- .../packaging/test/PackagingTestCase.java | 134 ++++++++++++++---- .../packaging/util/Archives.java | 134 +++++++++++------- .../elasticsearch/packaging/util/Docker.java | 2 +- .../packaging/util/Packages.java | 32 ++--- .../elasticsearch/packaging/util/Shell.java | 1 + 7 files changed, 238 insertions(+), 148 deletions(-) diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java index 37fd4448974af..d427124d7fecd 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java @@ -138,7 +138,7 @@ public void test50StartAndStop() throws Exception { rm(installation.config("elasticsearch.keystore")); try { - Archives.runElasticsearch(installation, sh); + startElasticsearch(); } catch (Exception e ){ if (Files.exists(installation.home.resolve("elasticsearch.pid"))) { String pid = FileUtils.slurp(installation.home.resolve("elasticsearch.pid")).trim(); @@ -151,7 +151,7 @@ public void test50StartAndStop() throws Exception { assertTrue("gc logs exist", Files.exists(installation.logs.resolve("gc.log"))); ServerUtils.runElasticsearchTests(); - Archives.stopElasticsearch(installation); + stopElasticsearch(); } public void test51JavaHomeOverride() throws Exception { @@ -164,9 +164,9 @@ public void test51JavaHomeOverride() throws Exception { sh.getEnv().put("JAVA_HOME", systemJavaHome1); }); - Archives.runElasticsearch(installation, sh); + startElasticsearch(); ServerUtils.runElasticsearchTests(); - Archives.stopElasticsearch(installation); + stopElasticsearch(); String systemJavaHome1 = sh.getEnv().get("JAVA_HOME"); assertThat(FileUtils.slurpAllLogs(installation.logs, "elasticsearch.log", "*.log.gz"), @@ -188,9 +188,9 @@ public void test52BundledJdkRemoved() throws Exception { sh.getEnv().put("JAVA_HOME", systemJavaHome1); }); - Archives.runElasticsearch(installation, sh); + startElasticsearch(); ServerUtils.runElasticsearchTests(); - Archives.stopElasticsearch(installation); + stopElasticsearch(); String systemJavaHome1 = sh.getEnv().get("JAVA_HOME"); assertThat(FileUtils.slurpAllLogs(installation.logs, "elasticsearch.log", "*.log.gz"), @@ -211,9 +211,9 @@ public void test53JavaHomeWithSpecialCharacters() throws Exception { sh.getEnv().put("JAVA_HOME", "C:\\Program Files (x86)\\java"); //verify ES can start, stop and run plugin list - Archives.runElasticsearch(installation, sh); + startElasticsearch(); - Archives.stopElasticsearch(installation); + stopElasticsearch(); String pluginListCommand = installation.bin + "/elasticsearch-plugin list"; Result result = sh.run(pluginListCommand); @@ -237,9 +237,9 @@ public void test53JavaHomeWithSpecialCharacters() throws Exception { sh.getEnv().put("JAVA_HOME", testJavaHome); //verify ES can start, stop and run plugin list - Archives.runElasticsearch(installation, sh); + startElasticsearch(); - Archives.stopElasticsearch(installation); + stopElasticsearch(); String pluginListCommand = installation.bin + "/elasticsearch-plugin list"; Result result = sh.run(pluginListCommand); @@ -284,13 +284,12 @@ public void test70CustomPathConfAndJvmOptions() throws Exception { "-Dlog4j2.disable.jmx=true\n"; append(tempConf.resolve("jvm.options"), jvmOptions); - final Shell sh = newShell(); sh.chown(tempConf); sh.getEnv().put("ES_PATH_CONF", tempConf.toString()); sh.getEnv().put("ES_JAVA_OPTS", "-XX:-UseCompressedOops"); - Archives.runElasticsearch(installation, sh); + startElasticsearch(); final String nodesResponse = makeRequest(Request.Get("http://localhost:9200/_nodes")); assertThat(nodesResponse, containsString("\"heap_init_in_bytes\":536870912")); @@ -318,17 +317,16 @@ public void test80RelativePathConf() throws Exception { append(tempConf.resolve("elasticsearch.yml"), "node.name: relative"); - final Shell sh = newShell(); sh.chown(temp); sh.setWorkingDirectory(temp); sh.getEnv().put("ES_PATH_CONF", "config"); - Archives.runElasticsearch(installation, sh); + startElasticsearch(); final String nodesResponse = makeRequest(Request.Get("http://localhost:9200/_nodes")); assertThat(nodesResponse, containsString("\"name\":\"relative\"")); - Archives.stopElasticsearch(installation); + stopElasticsearch(); } finally { rm(tempConf); @@ -393,7 +391,7 @@ public void test93ElasticsearchNodeCustomDataPathAndNotEsHomeWorkDir() throws Ex sh.setWorkingDirectory(getTempDir()); - Archives.runElasticsearch(installation, sh); + startElasticsearch(); Archives.stopElasticsearch(installation); Result result = sh.run("echo y | " + installation.executables().elasticsearchNode + " unsafe-bootstrap"); diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java index 155048be8a630..c992b02b1c522 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java @@ -50,9 +50,6 @@ import static org.elasticsearch.packaging.util.Packages.installPackage; import static org.elasticsearch.packaging.util.Packages.remove; import static org.elasticsearch.packaging.util.Packages.restartElasticsearch; -import static org.elasticsearch.packaging.util.Packages.startElasticsearch; -import static org.elasticsearch.packaging.util.Packages.startElasticsearchIgnoringFailure; -import static org.elasticsearch.packaging.util.Packages.stopElasticsearch; import static org.elasticsearch.packaging.util.Packages.verifyPackageInstallation; import static org.elasticsearch.packaging.util.Platforms.getOsRelease; import static org.elasticsearch.packaging.util.Platforms.isSystemd; @@ -101,9 +98,9 @@ private void assertRunsWithJavaHome() throws Exception { try { Files.write(installation.envFile, ("JAVA_HOME=" + systemJavaHome + "\n").getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND); - startElasticsearch(sh, installation); + startElasticsearch(); runElasticsearchTests(); - stopElasticsearch(sh); + stopElasticsearch(); } finally { Files.write(installation.envFile, originalEnvFile); } @@ -129,9 +126,9 @@ public void test33RunsIfJavaNotOnPath() throws Exception { } try { - startElasticsearch(sh, installation); + startElasticsearch(); runElasticsearchTests(); - stopElasticsearch(sh); + stopElasticsearch(); } finally { if (Files.exists(Paths.get(backupPath))) { sh.run("sudo mv " + backupPath + " /usr/bin/java"); @@ -153,7 +150,7 @@ public void test42BundledJdkRemoved() throws Exception { public void test40StartServer() throws Exception { String start = sh.runIgnoreExitCode("date ").stdout.trim(); - startElasticsearch(sh, installation); + startElasticsearch(); String journalEntries = sh.runIgnoreExitCode("journalctl _SYSTEMD_UNIT=elasticsearch.service " + "--since \"" + start + "\" --output cat | wc -l").stdout.trim(); @@ -218,7 +215,7 @@ public void test50Remove() throws Exception { } public void test60Reinstall() throws Exception { - installation = installPackage(distribution()); + install(); assertInstalled(distribution()); verifyPackageInstallation(installation, distribution(), sh); @@ -228,13 +225,13 @@ public void test60Reinstall() throws Exception { public void test70RestartServer() throws Exception { try { - installation = installPackage(distribution()); + install(); assertInstalled(distribution()); - startElasticsearch(sh, installation); + startElasticsearch(); restartElasticsearch(sh, installation); runElasticsearchTests(); - stopElasticsearch(sh); + stopElasticsearch(); } finally { cleanup(); } @@ -243,22 +240,22 @@ public void test70RestartServer() throws Exception { public void test72TestRuntimeDirectory() throws Exception { try { - installation = installPackage(distribution()); + install(); FileUtils.rm(installation.pidDir); - startElasticsearch(sh, installation); + startElasticsearch(); assertPathsExist(installation.pidDir); - stopElasticsearch(sh); + stopElasticsearch(); } finally { cleanup(); } } public void test73gcLogsExist() throws Exception { - installation = installPackage(distribution()); - startElasticsearch(sh, installation); + install(); + startElasticsearch(); // it can be gc.log or gc.log.0.current assertThat(installation.logs, fileWithGlobExist("gc.log*")); - stopElasticsearch(sh); + stopElasticsearch(); } // TEST CASES FOR SYSTEMD ONLY @@ -277,26 +274,26 @@ public void test80DeletePID_DIRandRestart() throws Exception { sh.run("systemd-tmpfiles --create"); - startElasticsearch(sh, installation); + startElasticsearch(); final Path pidFile = installation.pidDir.resolve("elasticsearch.pid"); assertTrue(Files.exists(pidFile)); - stopElasticsearch(sh); + stopElasticsearch(); } public void test81CustomPathConfAndJvmOptions() throws Exception { withCustomConfig(tempConf -> { append(installation.envFile, "ES_JAVA_OPTS=-XX:-UseCompressedOops"); - startElasticsearch(sh, installation); + startElasticsearch(); final String nodesResponse = makeRequest(Request.Get("http://localhost:9200/_nodes")); assertThat(nodesResponse, containsString("\"heap_init_in_bytes\":536870912")); assertThat(nodesResponse, containsString("\"using_compressed_ordinary_object_pointers\":\"false\"")); - stopElasticsearch(sh); + stopElasticsearch(); }); } @@ -306,7 +303,7 @@ public void test82SystemdMask() throws Exception { sh.run("systemctl mask systemd-sysctl.service"); - installation = installPackage(distribution()); + install(); sh.run("systemctl unmask systemd-sysctl.service"); } finally { @@ -318,9 +315,9 @@ public void test83serviceFileSetsLimits() throws Exception { // Limits are changed on systemd platforms only assumeTrue(isSystemd()); - installation = installPackage(distribution()); + install(); - startElasticsearch(sh, installation); + startElasticsearch(); final Path pidFile = installation.pidDir.resolve("elasticsearch.pid"); assertTrue(Files.exists(pidFile)); @@ -337,7 +334,7 @@ public void test83serviceFileSetsLimits() throws Exception { String maxAddressSpace = sh.run("cat /proc/%s/limits | grep \"Max address space\" | awk '{ print $4 }'", pid).stdout.trim(); assertThat(maxAddressSpace, equalTo("unlimited")); - stopElasticsearch(sh); + stopElasticsearch(); } public void test90DoNotCloseStderrWhenQuiet() throws Exception { @@ -347,7 +344,7 @@ public void test90DoNotCloseStderrWhenQuiet() throws Exception { // Make sure we don't pick up the journal entries for previous ES instances. clearJournal(sh); - startElasticsearchIgnoringFailure(sh); + runElasticsearchStartCommand(); final Result logs = sh.run("journalctl -u elasticsearch.service"); @@ -365,7 +362,7 @@ private void withCustomConfig(CustomConfigConsumer pathConsumer) throws Exceptio assertPathsExist(installation.envFile); - stopElasticsearch(sh); + stopElasticsearch(); // The custom config directory is not under /tmp or /var/tmp because // systemd's private temp directory functionally means different diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java index cb95408b2a5bf..f797d8d49684a 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java @@ -48,6 +48,8 @@ import java.nio.file.Paths; import static org.elasticsearch.packaging.util.Cleanup.cleanEverything; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; @@ -139,6 +141,9 @@ protected static void install() throws Exception { case DOCKER: installation = Docker.runContainer(distribution); Docker.verifyContainerInstallation(installation, distribution); + break; + default: + throw new IllegalStateException("Unknown Elasticsearch packaging type."); } } @@ -147,19 +152,7 @@ protected static void install() throws Exception { */ protected void assertWhileRunning(Platforms.PlatformAction assertions) throws Exception { try { - switch (distribution.packaging) { - case TAR: - case ZIP: - Archives.runElasticsearch(installation, sh); - break; - case DEB: - case RPM: - Packages.startElasticsearch(sh, installation); - break; - case DOCKER: - // nothing, "installing" docker image is running it - } - + awaitElasticsearchStartup(runElasticsearchStartCommand()); } catch (Exception e ){ if (Files.exists(installation.home.resolve("elasticsearch.pid"))) { String pid = FileUtils.slurp(installation.home.resolve("elasticsearch.pid")).trim(); @@ -180,7 +173,46 @@ protected void assertWhileRunning(Platforms.PlatformAction assertions) throws Ex FileUtils.slurpAllLogs(installation.logs, "elasticsearch.log", "*.log.gz")); throw e; } + stopElasticsearch(); + } + + protected static Shell newShell() throws Exception { + Shell sh = new Shell(); + if (distribution().hasJdk == false) { + Platforms.onLinux(() -> { + sh.getEnv().put("JAVA_HOME", systemJavaHome); + }); + Platforms.onWindows(() -> { + sh.getEnv().put("JAVA_HOME", systemJavaHome); + }); + } + return sh; + } + /** + * Run the command to start Elasticsearch, but don't wait or test for success. + * This method is useful for testing failure conditions in startup. To await success, + * use {@link #startElasticsearch()}. + * @return Shell results of the startup command. + * @throws Exception when command fails immediately. + */ + public Shell.Result runElasticsearchStartCommand() throws Exception { + switch (distribution.packaging) { + case TAR: + case ZIP: + return Archives.runElasticsearchStartCommand(installation, sh); + case DEB: + case RPM: + return Packages.runElasticsearchStartCommand(sh); + case DOCKER: + // nothing, "installing" docker image is running it + return Shell.NO_OP; + default: + throw new IllegalStateException("Unknown Elasticsearch packaging type."); + } + } + + public void stopElasticsearch() throws Exception { switch (distribution.packaging) { case TAR: case ZIP: @@ -191,20 +223,74 @@ protected void assertWhileRunning(Platforms.PlatformAction assertions) throws Ex Packages.stopElasticsearch(sh); break; case DOCKER: - // nothing, removing container is handled externally + // nothing, "installing" docker image is running it + break; + default: + throw new IllegalStateException("Unknown Elasticsearch packaging type."); } } - protected static Shell newShell() throws Exception { - Shell sh = new Shell(); - if (distribution().hasJdk == false) { - Platforms.onLinux(() -> { - sh.getEnv().put("JAVA_HOME", systemJavaHome); - }); - Platforms.onWindows(() -> { - sh.getEnv().put("JAVA_HOME", systemJavaHome); - }); + public void awaitElasticsearchStartup(Shell.Result result) throws Exception { + assertThat("Startup command should succeed", result.exitCode, equalTo(0)); + switch (distribution.packaging) { + case TAR: + case ZIP: + Archives.assertElasticsearchStarted(installation); + break; + case DEB: + case RPM: + Packages.assertElasticsearchStarted(sh, installation); + break; + case DOCKER: + Docker.waitForElasticsearchToStart(); + break; + default: + throw new IllegalStateException("Unknown Elasticsearch packaging type."); + } + } + + /** + * Start Elasticsearch and wait until it's up and running. If you just want to run + * the start command, use {@link #runElasticsearchStartCommand()}. + * @throws Exception if Elasticsearch can't start + */ + public void startElasticsearch() throws Exception { + awaitElasticsearchStartup(runElasticsearchStartCommand()); + } + + public void assertElasticsearchFailure(Shell.Result result, String expectedMessage) { + + if (Files.exists(installation.logs.resolve("elasticsearch.log"))) { + + // If log file exists, then we have bootstrapped our logging and the + // error should be in the logs + assertTrue("log file exists", Files.exists(installation.logs.resolve("elasticsearch.log"))); + String logfile = FileUtils.slurp(installation.logs.resolve("elasticsearch.log")); + assertThat(logfile, containsString(expectedMessage)); + + } else if (distribution().isPackage() && Platforms.isSystemd()) { + + // For systemd, retrieve the error from journalctl + assertThat(result.stderr, containsString("Job for elasticsearch.service failed")); + Shell.Result error = sh.run("journalctl --boot --unit elasticsearch.service"); + assertThat(error.stdout, containsString(expectedMessage)); + + } else if (Platforms.WINDOWS == true) { + + // In Windows, we have written our stdout and stderr to files in order to run + // in the background + String wrapperPid = result.stdout.trim(); + sh.runIgnoreExitCode("Wait-Process -Timeout " + Archives.ES_STARTUP_SLEEP_TIME_SECONDS + " -Id " + wrapperPid); + sh.runIgnoreExitCode("Get-EventSubscriber | " + + "where {($_.EventName -eq 'OutputDataReceived' -Or $_.EventName -eq 'ErrorDataReceived' |" + + "Unregister-EventSubscriber -Force"); + assertThat(FileUtils.slurp(Archives.getPowershellErrorPath(installation)), containsString(expectedMessage)); + + } else { + + // Otherwise, error should be on shell stderr + assertThat(result.stderr, containsString(expectedMessage)); + } - return sh; } } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java index 4303ca3bb6dd1..1ec62d20a050d 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java @@ -43,7 +43,7 @@ import static org.elasticsearch.packaging.util.FileUtils.slurp; import static org.elasticsearch.packaging.util.Platforms.isDPKG; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.emptyOrNullString; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.collection.IsEmptyCollection.empty; import static org.hamcrest.core.Is.is; @@ -63,6 +63,10 @@ public class Archives { ? System.getenv("username") : "elasticsearch"; + /** This is an arbitrarily chosen value that gives Elasticsearch time to log Bootstrap + * errors to the console if they occur before the logging framework is initialized. */ + public static final String ES_STARTUP_SLEEP_TIME_SECONDS = "10"; + public static Installation installArchive(Distribution distribution) throws Exception { return installArchive(distribution, getDefaultArchiveInstallPath(), getCurrentVersion()); } @@ -242,89 +246,109 @@ private static void verifyDefaultInstallation(Installation es, Distribution dist ).forEach(configFile -> assertThat(es.config(configFile), file(File, owner, owner, p660))); } - public static void runElasticsearch(Installation installation, Shell sh) throws Exception { + public static Shell.Result runElasticsearchStartCommand(Installation installation, Shell sh) { final Path pidFile = installation.home.resolve("elasticsearch.pid"); assertFalse("Pid file doesn't exist when starting Elasticsearch", Files.exists(pidFile)); final Installation.Executables bin = installation.executables(); - Platforms.onLinux(() -> { + if (Platforms.WINDOWS == false) { // If jayatana is installed then we try to use it. Elasticsearch should ignore it even when we try. // If it doesn't ignore it then Elasticsearch will fail to start because of security errors. // This line is attempting to emulate the on login behavior of /usr/share/upstart/sessions/jayatana.conf if (Files.exists(Paths.get("/usr/share/java/jayatanaag.jar"))) { sh.getEnv().put("JAVA_TOOL_OPTIONS", "-javaagent:/usr/share/java/jayatanaag.jar"); } - sh.run("sudo -E -u " + ARCHIVE_OWNER + " " + - bin.elasticsearch + " -d -p " + installation.home.resolve("elasticsearch.pid")); - }); - Platforms.onWindows(() -> { - // this starts the server in the background. the -d flag is unsupported on windows - if (System.getenv("username").equals("vagrant")) { - // these tests run as Administrator in vagrant. - // we don't want to run the server as Administrator, so we provide the current user's - // username and password to the process which has the effect of starting it not as Administrator. - sh.run( - "$password = ConvertTo-SecureString 'vagrant' -AsPlainText -Force; " + - "$processInfo = New-Object System.Diagnostics.ProcessStartInfo; " + - "$processInfo.FileName = '" + bin.elasticsearch + "'; " + - "$processInfo.Arguments = '-p " + installation.home.resolve("elasticsearch.pid") + "'; " + - "$processInfo.Username = 'vagrant'; " + - "$processInfo.Password = $password; " + - "$processInfo.RedirectStandardOutput = $true; " + - "$processInfo.RedirectStandardError = $true; " + - sh.env.entrySet().stream() - .map(entry -> "$processInfo.Environment.Add('" + entry.getKey() + "', '" + entry.getValue() + "'); ") - .collect(joining()) + - "$processInfo.UseShellExecute = $false; " + - "$process = New-Object System.Diagnostics.Process; " + - "$process.StartInfo = $processInfo; " + - "$process.Start() | Out-Null; " + - "$process.Id;" - ); - } else { - sh.run( - "$processInfo = New-Object System.Diagnostics.ProcessStartInfo; " + - "$processInfo.FileName = '" + bin.elasticsearch + "'; " + - "$processInfo.Arguments = '-p " + installation.home.resolve("elasticsearch.pid") + "'; " + - "$processInfo.RedirectStandardOutput = $true; " + - "$processInfo.RedirectStandardError = $true; " + - sh.env.entrySet().stream() - .map(entry -> "$processInfo.Environment.Add('" + entry.getKey() + "', '" + entry.getValue() + "'); ") - .collect(joining()) + - "$processInfo.UseShellExecute = $false; " + - "$process = New-Object System.Diagnostics.Process; " + - "$process.StartInfo = $processInfo; " + - "$process.Start() | Out-Null; " + - "$process.Id;" - ); - } - }); + // We need to give Elasticsearch enough time to print failures to stderr before exiting + sh.getEnv().put("ES_STARTUP_SLEEP_TIME", ES_STARTUP_SLEEP_TIME_SECONDS); + return sh.runIgnoreExitCode("sudo -E -u " + ARCHIVE_OWNER + " " + bin.elasticsearch + " -d -p " + pidFile); + } + final Path stdout = getPowershellOutputPath(installation); + final Path stderr = getPowershellErrorPath(installation); + + String powerShellProcessUserSetup; + if (System.getenv("username").equals("vagrant")) { + // the tests will run as Administrator in vagrant. + // we don't want to run the server as Administrator, so we provide the current user's + // username and password to the process which has the effect of starting it not as Administrator. + powerShellProcessUserSetup = + "$password = ConvertTo-SecureString 'vagrant' -AsPlainText -Force; " + + "$processInfo.Username = 'vagrant'; " + + "$processInfo.Password = $password; "; + } else { + powerShellProcessUserSetup = ""; + } + + // this starts the server in the background. the -d flag is unsupported on windows + return sh.run( + "$processInfo = New-Object System.Diagnostics.ProcessStartInfo; " + + "$processInfo.FileName = '" + bin.elasticsearch + "'; " + + "$processInfo.Arguments = '-p " + installation.home.resolve("elasticsearch.pid") + "'; " + + powerShellProcessUserSetup + + "$processInfo.RedirectStandardOutput = $true; " + + "$processInfo.RedirectStandardError = $true; " + + "$processInfo.RedirectStandardInput = $true; " + + sh.env.entrySet().stream() + .map(entry -> "$processInfo.Environment.Add('" + entry.getKey() + "', '" + entry.getValue() + "'); ") + .collect(joining()) + + "$processInfo.UseShellExecute = $false; " + + "$process = New-Object System.Diagnostics.Process; " + + "$process.StartInfo = $processInfo; " + + + // set up some asynchronous output handlers + "$outScript = { $EventArgs.Data | Out-File -Encoding UTF8 -Append '" + stdout + "' }; " + + "$errScript = { $EventArgs.Data | Out-File -Encoding UTF8 -Append '" + stderr + "' }; " + + "$stdOutEvent = Register-ObjectEvent -InputObject $process " + + "-Action $outScript -EventName 'OutputDataReceived'; " + + "$stdErrEvent = Register-ObjectEvent -InputObject $process " + + "-Action $errScript -EventName 'ErrorDataReceived'; " + + + "$process.Start() | Out-Null; " + + "$process.BeginOutputReadLine(); " + + "$process.BeginErrorReadLine(); " + + "Wait-Process -Timeout " + ES_STARTUP_SLEEP_TIME_SECONDS + " -Id $process.Id; " + + "$process.Id;" + ); + } + public static void assertElasticsearchStarted(Installation installation) throws Exception { + final Path pidFile = installation.home.resolve("elasticsearch.pid"); ServerUtils.waitForElasticsearch(installation); assertTrue("Starting Elasticsearch produced a pid file at " + pidFile, Files.exists(pidFile)); String pid = slurp(pidFile).trim(); - assertThat(pid, not(isEmptyOrNullString())); - - Platforms.onLinux(() -> sh.run("ps " + pid)); - Platforms.onWindows(() -> sh.run("Get-Process -Id " + pid)); + assertThat(pid, is(not(emptyOrNullString()))); } public static void stopElasticsearch(Installation installation) throws Exception { Path pidFile = installation.home.resolve("elasticsearch.pid"); - assertTrue(Files.exists(pidFile)); + assertTrue("pid file should exist", Files.exists(pidFile)); String pid = slurp(pidFile).trim(); - assertThat(pid, not(isEmptyOrNullString())); + assertThat(pid, is(not(emptyOrNullString()))); final Shell sh = new Shell(); Platforms.onLinux(() -> sh.run("kill -SIGTERM " + pid + "; tail --pid=" + pid + " -f /dev/null")); - Platforms.onWindows(() -> sh.run("Get-Process -Id " + pid + " | Stop-Process -Force; Wait-Process -Id " + pid)); + Platforms.onWindows(() -> { + sh.run("Get-Process -Id " + pid + " | Stop-Process -Force; Wait-Process -Id " + pid); + + // Clear the asynchronous event handlers + sh.runIgnoreExitCode("Get-EventSubscriber | " + + "where {($_.EventName -eq 'OutputDataReceived' -Or $_.EventName -eq 'ErrorDataReceived' |" + + "Unregister-EventSubscriber -Force"); + }); if (Files.exists(pidFile)) { Files.delete(pidFile); } } + public static Path getPowershellErrorPath(Installation installation) { + return installation.logs.resolve("output.err"); + } + + private static Path getPowershellOutputPath(Installation installation) { + return installation.logs.resolve("output.out"); + } + } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java index 95e5b586627c6..91b010957011b 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java @@ -166,7 +166,7 @@ private static void executeDockerRun(Distribution distribution, Map * Waits for the Elasticsearch process to start executing in the container. * This is called every time a container is started. */ - private static void waitForElasticsearchToStart() { + public static void waitForElasticsearchToStart() { boolean isElasticsearchRunning = false; int attempt = 0; diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Packages.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Packages.java index 723625f1bb8bc..a96c7d1b6707a 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Packages.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Packages.java @@ -268,32 +268,17 @@ private static void verifyDefaultInstallation(Installation es) { ).forEach(configFile -> assertThat(es.config(configFile), file(File, "root", "elasticsearch", p660))); } - public static void startElasticsearch(Shell sh, Installation installation) throws IOException { + /** + * Starts Elasticsearch, without checking that startup is successful. + */ + public static Shell.Result runElasticsearchStartCommand(Shell sh) throws IOException { if (isSystemd()) { sh.run("systemctl daemon-reload"); sh.run("systemctl enable elasticsearch.service"); sh.run("systemctl is-enabled elasticsearch.service"); - sh.run("systemctl start elasticsearch.service"); - } else { - sh.run("service elasticsearch start"); - } - - assertElasticsearchStarted(sh, installation); - } - - /** - * Starts Elasticsearch, without checking that startup is successful. To also check - * that Elasticsearch has started, call {@link #startElasticsearch(Shell, Installation)}. - */ - public static void startElasticsearchIgnoringFailure(Shell sh) { - if (isSystemd()) { - sh.runIgnoreExitCode("systemctl daemon-reload"); - sh.runIgnoreExitCode("systemctl enable elasticsearch.service"); - sh.runIgnoreExitCode("systemctl is-enabled elasticsearch.service"); - sh.runIgnoreExitCode("systemctl start elasticsearch.service"); - } else { - sh.runIgnoreExitCode("service elasticsearch start"); + return sh.runIgnoreExitCode("systemctl start elasticsearch.service"); } + return sh.runIgnoreExitCode("service elasticsearch start"); } /** @@ -321,7 +306,7 @@ public static void clearJournal(Shell sh) { } } - private static void assertElasticsearchStarted(Shell sh, Installation installation) throws IOException { + public static void assertElasticsearchStarted(Shell sh, Installation installation) throws IOException { waitForElasticsearch(installation); if (isSystemd()) { @@ -346,7 +331,6 @@ public static void restartElasticsearch(Shell sh, Installation installation) thr } else { sh.run("service elasticsearch restart"); } - - waitForElasticsearch(installation); + assertElasticsearchStarted(sh, installation); } } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Shell.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Shell.java index 55488522797c1..bbcbcd1b1d498 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Shell.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Shell.java @@ -43,6 +43,7 @@ public class Shell { public static final int TAIL_WHEN_TOO_MUCH_OUTPUT = 1000; + public static final Result NO_OP = new Shell.Result(0, "",""); protected final Logger logger = LogManager.getLogger(getClass()); final Map env = new HashMap<>(); From 1f8c96a6a1437613fa7c62c7a5548e38ac299a76 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Mon, 9 Dec 2019 18:47:27 -0800 Subject: [PATCH 133/686] Added license and renamed files for tests --- .../xpack/core/ilm/CloseIndexStep.java | 19 +++-------------- .../xpack/core/ilm/OpenIndexStep.java | 20 ++++-------------- .../xpack/core/ilm/WaitForIndexGreenStep.java | 20 ++++-------------- ...StepTest.java => CloseIndexStepTests.java} | 21 ++++--------------- ...xStepTest.java => OpenIndexStepTests.java} | 21 ++++--------------- ...t.java => WaitForIndexGreenStepTests.java} | 21 ++++--------------- .../ilm/TimeSeriesLifecycleActionsIT.java | 4 ++-- 7 files changed, 25 insertions(+), 101 deletions(-) rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/{CloseIndexStepTest.java => CloseIndexStepTests.java} (87%) rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/{OpenIndexStepTest.java => OpenIndexStepTests.java} (87%) rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/{WaitForIndexGreenStepTest.java => WaitForIndexGreenStepTests.java} (88%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java index 53f7e469c3bbe..f0afab5f604ee 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ package org.elasticsearch.xpack.core.ilm; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java index fc9cf75284411..b032b0761fa44 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java @@ -1,21 +1,9 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ + package org.elasticsearch.xpack.core.ilm; import org.elasticsearch.action.ActionListener; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java index fc602a1a250f7..d717a970c66b1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java @@ -1,21 +1,9 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ + package org.elasticsearch.xpack.core.ilm; import com.carrotsearch.hppc.cursors.ObjectCursor; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java similarity index 87% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java index 53b2c47d00b69..b0980b284eb73 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ package org.elasticsearch.xpack.core.ilm; @@ -36,7 +23,7 @@ import static org.hamcrest.Matchers.equalTo; -public class CloseIndexStepTest extends AbstractStepTestCase { +public class CloseIndexStepTests extends AbstractStepTestCase { private Client client; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTests.java similarity index 87% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTests.java index 558e4ac824a11..812d90dd5c259 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTests.java @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ package org.elasticsearch.xpack.core.ilm; @@ -34,7 +21,7 @@ import static org.hamcrest.Matchers.equalTo; -public class OpenIndexStepTest extends AbstractStepTestCase { +public class OpenIndexStepTests extends AbstractStepTestCase { private Client client; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java similarity index 88% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java index 87c1f19a56e4f..46fba4bcf1770 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTest.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ package org.elasticsearch.xpack.core.ilm; @@ -36,7 +23,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; -public class WaitForIndexGreenStepTest extends AbstractStepTestCase { +public class WaitForIndexGreenStepTests extends AbstractStepTestCase { @Override protected WaitForIndexGreenStep createRandomInstance() { diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 4e160c69efd13..2ee3de617c432 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -411,7 +411,7 @@ public void testForceMergeAction() throws Exception { }; assertThat(numSegments.get(), greaterThan(1)); - createNewSingletonPolicy("warm", new ForceMergeAction(1)); + createNewSingletonPolicy("warm", new ForceMergeAction(1, false)); updatePolicy(index, policy); assertBusy(() -> { @@ -1007,7 +1007,7 @@ private void createFullPolicy(TimeValue hotTime) throws IOException { hotActions.put(RolloverAction.NAME, new RolloverAction(null, null, 1L)); Map warmActions = new HashMap<>(); warmActions.put(SetPriorityAction.NAME, new SetPriorityAction(50)); - warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1)); + warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, false)); warmActions.put(AllocateAction.NAME, new AllocateAction(1, singletonMap("_name", "integTest-1,integTest-2"), null, null)); warmActions.put(ShrinkAction.NAME, new ShrinkAction(1)); Map coldActions = new HashMap<>(); From d12c7cb4700cfa8167f98d4ff7d1dbceeed8542c Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 10 Dec 2019 16:46:07 +1100 Subject: [PATCH 134/686] Add setting to restrict license types (#49418) This adds a new "xpack.license.upload.types" setting that restricts which license types may be uploaded to a cluster. By default all types are allowed (excluding basic, which can only be generated and never uploaded). This setting does not restrict APIs that generate licenses such as the start trial API. This setting is not documented as it is intended to be set by orchestrators and not end users. --- .../org/elasticsearch/license/License.java | 49 ++++-- .../elasticsearch/license/LicenseService.java | 49 +++++- .../license/OperationModeFileWatcher.java | 2 +- .../license/RemoteClusterLicenseChecker.java | 6 +- .../xpack/core/XPackClientPlugin.java | 1 + .../core/ml/inference/TrainedModelConfig.java | 4 +- .../license/LicenseFIPSTests.java | 10 ++ .../license/LicenseOperationModeTests.java | 6 +- .../LicenseOperationModeUpdateTests.java | 2 +- .../license/LicenseServiceTests.java | 164 ++++++++++++++++++ 10 files changed, 273 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java index 6731518f5b534..004c9ff987764 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java @@ -63,7 +63,23 @@ public static LicenseType parse(String type) throws IllegalArgumentException { /** * Backward compatible license type parsing for older license models */ - public static LicenseType resolve(String name) { + public static LicenseType resolve(License license) { + if (license.version == VERSION_START) { + // in 1.x: the acceptable values for 'subscription_type': none | dev | silver | gold | platinum + return resolve(license.subscriptionType); + } else { + // in 2.x: the acceptable values for 'type': trial | basic | silver | dev | gold | platinum + // in 5.x: the acceptable values for 'type': trial | basic | standard | dev | gold | platinum + // in 6.x: the acceptable values for 'type': trial | basic | standard | dev | gold | platinum + // in 7.x: the acceptable values for 'type': trial | basic | standard | dev | gold | platinum | enterprise + return resolve(license.type); + } + } + + /** + * Backward compatible license type parsing for older license models + */ + static LicenseType resolve(String name) { switch (name.toLowerCase(Locale.ROOT)) { case "missing": return null; @@ -165,8 +181,12 @@ public static int compare(OperationMode opMode1, OperationMode opMode2) { return Integer.compare(opMode1.id, opMode2.id); } - public static OperationMode resolve(String typeName) { - LicenseType type = LicenseType.resolve(typeName); + /** + * Determine the operating mode for a license type + * @see LicenseType#resolve(License) + * @see #parse(String) + */ + public static OperationMode resolve(LicenseType type) { if (type == null) { return MISSING; } @@ -187,6 +207,21 @@ public static OperationMode resolve(String typeName) { } } + /** + * Parses an {@code OperatingMode} from a String. + * The string must name an operating mode, and not a licensing level (that is, it cannot parse old style license levels + * such as "dev" or "silver"). + * @see #description() + */ + public static OperationMode parse(String mode) { + try { + return OperationMode.valueOf(mode.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("unrecognised license operating mode [ " + mode + "], supported modes are [" + + Stream.of(values()).map(OperationMode::description).collect(Collectors.joining(",")) + "]"); + } + } + public String description() { return name().toLowerCase(Locale.ROOT); } @@ -212,13 +247,7 @@ private License(int version, String uid, String issuer, String issuedTo, long is } this.maxNodes = maxNodes; this.startDate = startDate; - if (version == VERSION_START) { - // in 1.x: the acceptable values for 'subscription_type': none | dev | silver | gold | platinum - this.operationMode = OperationMode.resolve(subscriptionType); - } else { - // in 2.x: the acceptable values for 'type': trial | basic | silver | dev | gold | platinum - this.operationMode = OperationMode.resolve(type); - } + this.operationMode = OperationMode.resolve(LicenseType.resolve(this)); validate(); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java index f16cb2fbe3932..af34d31c14422 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java @@ -47,6 +47,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Service responsible for managing {@link LicensesMetaData}. @@ -64,6 +65,12 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste return SelfGeneratedLicense.validateSelfGeneratedType(type); }, Setting.Property.NodeScope); + static final List ALLOWABLE_UPLOAD_TYPES = getAllowableUploadTypes(); + + public static final Setting> ALLOWED_LICENSE_TYPES_SETTING = Setting.listSetting("xpack.license.upload.types", + ALLOWABLE_UPLOAD_TYPES.stream().map(License.LicenseType::getTypeName).collect(Collectors.toUnmodifiableList()), + License.LicenseType::parse, LicenseService::validateUploadTypesSetting, Setting.Property.NodeScope); + // pkg private for tests static final TimeValue NON_BASIC_SELF_GENERATED_LICENSE_DURATION = TimeValue.timeValueHours(30 * 24); @@ -104,6 +111,12 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste */ private List expirationCallbacks = new ArrayList<>(); + /** + * Which license types are permitted to be uploaded to the cluster + * @see #ALLOWED_LICENSE_TYPES_SETTING + */ + private final List allowedLicenseTypes; + /** * Max number of nodes licensed by generated trial license */ @@ -123,6 +136,7 @@ public LicenseService(Settings settings, ClusterService clusterService, Clock cl this.clock = clock; this.scheduler = new SchedulerEngine(settings, clock); this.licenseState = licenseState; + this.allowedLicenseTypes = ALLOWED_LICENSE_TYPES_SETTING.get(settings); this.operationModeFileWatcher = new OperationModeFileWatcher(resourceWatcherService, XPackPlugin.resolveConfigFile(env, "license_mode"), logger, () -> updateLicenseState(getLicensesMetaData())); @@ -196,8 +210,20 @@ public void registerLicense(final PutLicenseRequest request, final ActionListene final long now = clock.millis(); if (!LicenseVerifier.verifyLicense(newLicense) || newLicense.issueDate() > now || newLicense.startDate() > now) { listener.onResponse(new PutLicenseResponse(true, LicensesStatus.INVALID)); - } else if (newLicense.type().equals(License.LicenseType.BASIC.getTypeName())) { + return; + } + final License.LicenseType licenseType; + try { + licenseType = License.LicenseType.resolve(newLicense); + } catch (Exception e) { + listener.onFailure(e); + return; + } + if (licenseType == License.LicenseType.BASIC) { listener.onFailure(new IllegalArgumentException("Registering basic licenses is not allowed.")); + } else if (isAllowedLicenseType(licenseType) == false) { + listener.onFailure(new IllegalArgumentException( + "Registering [" + licenseType.getTypeName() + "] licenses is not allowed on this cluster")); } else if (newLicense.expiryDate() < now) { listener.onResponse(new PutLicenseResponse(true, LicensesStatus.EXPIRED)); } else { @@ -272,6 +298,11 @@ private static boolean licenseIsCompatible(License license, Version version) { } } + private boolean isAllowedLicenseType(License.LicenseType type) { + logger.debug("Checking license [{}] against allowed license types: {}", type, allowedLicenseTypes); + return allowedLicenseTypes.contains(type); + } + public static Map getAckMessages(License newLicense, License currentLicense) { Map acknowledgeMessages = new HashMap<>(); if (!License.isAutoGeneratedLicense(currentLicense.signature()) // current license is not auto-generated @@ -574,4 +605,20 @@ private static boolean isProductionMode(Settings settings, DiscoveryNode localNo private static boolean isBoundToLoopback(DiscoveryNode localNode) { return localNode.getAddress().address().getAddress().isLoopbackAddress(); } + + private static List getAllowableUploadTypes() { + return Stream.of(License.LicenseType.values()) + .filter(t -> t != License.LicenseType.BASIC) + .collect(Collectors.toUnmodifiableList()); + } + + private static void validateUploadTypesSetting(List value) { + if (ALLOWABLE_UPLOAD_TYPES.containsAll(value) == false) { + throw new IllegalArgumentException("Invalid value [" + + value.stream().map(License.LicenseType::getTypeName).collect(Collectors.joining(",")) + + "] for " + ALLOWED_LICENSE_TYPES_SETTING.getKey() + ", allowed values are [" + + ALLOWABLE_UPLOAD_TYPES.stream().map(License.LicenseType::getTypeName).collect(Collectors.joining(",")) + + "]"); + } + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/OperationModeFileWatcher.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/OperationModeFileWatcher.java index b8e6446b9f49f..ee08b9f7330cf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/OperationModeFileWatcher.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/OperationModeFileWatcher.java @@ -106,7 +106,7 @@ private synchronized void onChange(Path file) { // this UTF-8 conversion is much pickier than java String final String operationMode = new BytesRef(content).utf8ToString(); try { - newOperationMode = OperationMode.resolve(operationMode); + newOperationMode = OperationMode.parse(operationMode); } catch (IllegalArgumentException e) { logger.error( (Supplier) () -> new ParameterizedMessage( diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java index 7d5a3b5e9a53d..5de1186767f4b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java @@ -138,7 +138,7 @@ public RemoteClusterLicenseChecker(final Client client, final Predicate> getSettings() { settings.addAll(XPackSettings.getAllSettings()); settings.add(LicenseService.SELF_GENERATED_LICENSE_TYPE); + settings.add(LicenseService.ALLOWED_LICENSE_TYPES_SETTING); // we add the `xpack.version` setting to all internal indices settings.add(Setting.simpleString("index.xpack.version", Setting.Property.IndexScope)); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/TrainedModelConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/TrainedModelConfig.java index 21e145546f8b7..343a520d9b5d3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/TrainedModelConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/TrainedModelConfig.java @@ -138,7 +138,7 @@ public static TrainedModelConfig.Builder fromXContent(XContentParser parser, boo throw new IllegalArgumentException("[" + ESTIMATED_OPERATIONS.getPreferredName() + "] must be greater than or equal to 0"); } this.estimatedOperations = estimatedOperations; - this.licenseLevel = License.OperationMode.resolve(ExceptionsHelper.requireNonNull(licenseLevel, LICENSE_LEVEL)); + this.licenseLevel = License.OperationMode.parse(ExceptionsHelper.requireNonNull(licenseLevel, LICENSE_LEVEL)); } public TrainedModelConfig(StreamInput in) throws IOException { @@ -153,7 +153,7 @@ public TrainedModelConfig(StreamInput in) throws IOException { input = new TrainedModelInput(in); estimatedHeapMemory = in.readVLong(); estimatedOperations = in.readVLong(); - licenseLevel = License.OperationMode.resolve(in.readString()); + licenseLevel = License.OperationMode.parse(in.readString()); } public String getModelId() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java index c432a207fcb70..eb357661d50ca 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java @@ -34,6 +34,11 @@ public void testFIPSCheckWithAllowedLicense() throws Exception { licenseService.start(); PlainActionFuture responseFuture = new PlainActionFuture<>(); licenseService.registerLicense(request, responseFuture); + if (responseFuture.isDone()) { + // If the future is done, it means request/license validation failed. + // In which case, this `actionGet` should throw a more useful exception than the verify below. + responseFuture.actionGet(); + } verify(clusterService).submitStateUpdateTask(any(String.class), any(ClusterStateUpdateTask.class)); } @@ -67,6 +72,11 @@ public void testFIPSCheckWithoutAllowedLicense() throws Exception { setInitialState(null, licenseState, settings); licenseService.start(); licenseService.registerLicense(request, responseFuture); + if (responseFuture.isDone()) { + // If the future is done, it means request/license validation failed. + // In which case, this `actionGet` should throw a more useful exception than the verify below. + responseFuture.actionGet(); + } verify(clusterService).submitStateUpdateTask(any(String.class), any(ClusterStateUpdateTask.class)); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeTests.java index 648f48ff2ea13..a1fbfbe6c6a41 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeTests.java @@ -57,7 +57,8 @@ public void testResolveUnknown() { for (String type : types) { try { - OperationMode.resolve(type); + final License.LicenseType licenseType = License.LicenseType.resolve(type); + OperationMode.resolve(licenseType); fail(String.format(Locale.ROOT, "[%s] should not be recognized as an operation mode", type)); } @@ -69,7 +70,8 @@ public void testResolveUnknown() { private static void assertResolve(OperationMode expected, String... types) { for (String type : types) { - assertThat(OperationMode.resolve(type), equalTo(expected)); + License.LicenseType licenseType = License.LicenseType.resolve(type); + assertThat(OperationMode.resolve(licenseType), equalTo(expected)); } } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeUpdateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeUpdateTests.java index a69331287918b..20df885261fed 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeUpdateTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeUpdateTests.java @@ -34,7 +34,7 @@ public void init() throws Exception { } public void testLicenseOperationModeUpdate() throws Exception { - String type = randomFrom("trial", "basic", "standard", "gold", "platinum"); + License.LicenseType type = randomFrom(License.LicenseType.values()); License license = License.builder() .uid("id") .expiryDate(0) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java index 750b3d67c5f62..b1b22f15c259f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java @@ -6,12 +6,47 @@ package org.elasticsearch.license; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.AckedClusterStateUpdateTask; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateUpdateTask; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.bytes.BytesReference; +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.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.licensor.LicenseSigner; +import org.elasticsearch.protocol.xpack.license.LicensesStatus; +import org.elasticsearch.protocol.xpack.license.PutLicenseResponse; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.TestMatchers; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Clock; import java.time.LocalDate; import java.time.ZoneOffset; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.startsWith; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * Due to changes in JDK9 where locale data is used from CLDR, the licence message will differ in jdk 8 and jdk9+ @@ -30,4 +65,133 @@ public void testLogExpirationWarning() { assertThat(message, startsWith("License [will expire] on [Thursday, November 15, 2018].\n")); } } + + /** + * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES_SETTING} is on its default value (all license types) + */ + public void testRegisterLicenseWithoutTypeRestrictions() throws Exception { + assertRegisterValidLicense(Settings.EMPTY, + randomValueOtherThan(License.LicenseType.BASIC, () -> randomFrom(License.LicenseType.values()))); + } + + /** + * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES_SETTING} is set, + * and the uploaded license type matches + */ + public void testSuccessfullyRegisterLicenseMatchingTypeRestrictions() throws Exception { + final List allowed = randomSubsetOf( + randomIntBetween(1, LicenseService.ALLOWABLE_UPLOAD_TYPES.size() - 1), LicenseService.ALLOWABLE_UPLOAD_TYPES); + final List allowedNames = allowed.stream().map(License.LicenseType::getTypeName).collect(Collectors.toUnmodifiableList()); + final Settings settings = Settings.builder() + .putList("xpack.license.upload.types", allowedNames) + .build(); + assertRegisterValidLicense(settings, randomFrom(allowed)); + } + + /** + * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES_SETTING} is set, + * and the uploaded license type does not match + */ + public void testFailToRegisterLicenseNotMatchingTypeRestrictions() throws Exception { + final List allowed = randomSubsetOf( + randomIntBetween(1, LicenseService.ALLOWABLE_UPLOAD_TYPES.size() - 2), LicenseService.ALLOWABLE_UPLOAD_TYPES); + final List allowedNames = allowed.stream().map(License.LicenseType::getTypeName).collect(Collectors.toUnmodifiableList()); + final Settings settings = Settings.builder() + .putList("xpack.license.upload.types", allowedNames) + .build(); + final License.LicenseType notAllowed = randomValueOtherThanMany( + test -> allowed.contains(test), + () -> randomFrom(LicenseService.ALLOWABLE_UPLOAD_TYPES)); + assertRegisterDisallowedLicenseType(settings, notAllowed); + } + + private void assertRegisterValidLicense(Settings baseSettings, License.LicenseType licenseType) throws IOException { + tryRegisterLicense(baseSettings, licenseType, + future -> assertThat(future.actionGet().status(), equalTo(LicensesStatus.VALID))); + } + + private void assertRegisterDisallowedLicenseType(Settings baseSettings, License.LicenseType licenseType) throws IOException { + tryRegisterLicense(baseSettings, licenseType, future -> { + final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, future::actionGet); + assertThat(exception, TestMatchers.throwableWithMessage( + "Registering [" + licenseType.getTypeName() + "] licenses is not allowed on " + "this cluster")); + }); + } + + private void tryRegisterLicense(Settings baseSettings, License.LicenseType licenseType, + Consumer> assertion) throws IOException { + final Settings settings = Settings.builder() + .put(baseSettings) + .put("path.home", createTempDir()) + .put("discovery.type", "single-node") // So we skip TLS checks + .build(); + + final ClusterState clusterState = Mockito.mock(ClusterState.class); + Mockito.when(clusterState.metaData()).thenReturn(MetaData.EMPTY_META_DATA); + + final ClusterService clusterService = Mockito.mock(ClusterService.class); + Mockito.when(clusterService.state()).thenReturn(clusterState); + + final Clock clock = randomBoolean() ? Clock.systemUTC() : Clock.systemDefaultZone(); + final Environment env = TestEnvironment.newEnvironment(settings); + final ResourceWatcherService resourceWatcherService = Mockito.mock(ResourceWatcherService.class); + final XPackLicenseState licenseState = Mockito.mock(XPackLicenseState.class); + final LicenseService service = new LicenseService(settings, clusterService, clock, env, resourceWatcherService, licenseState); + + final PutLicenseRequest request = new PutLicenseRequest(); + request.license(spec(licenseType, TimeValue.timeValueDays(randomLongBetween(1, 1000))), XContentType.JSON); + final PlainActionFuture future = new PlainActionFuture<>(); + service.registerLicense(request, future); + + if (future.isDone()) { + // If validation failed, the future might be done without calling the updater task. + assertion.accept(future); + } else { + ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(ClusterStateUpdateTask.class); + verify(clusterService, times(1)).submitStateUpdateTask(any(), taskCaptor.capture()); + + final ClusterStateUpdateTask task = taskCaptor.getValue(); + assertThat(task, instanceOf(AckedClusterStateUpdateTask.class)); + ((AckedClusterStateUpdateTask) task).onAllNodesAcked(null); + + assertion.accept(future); + } + } + + private BytesReference spec(License.LicenseType type, TimeValue expires) throws IOException { + final License signed = sign(buildLicense(type, expires)); + return toSpec(signed); + } + + private BytesReference toSpec(License license) throws IOException { + XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON); + builder.startObject(); + builder.startObject("license"); + license.toInnerXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + builder.endObject(); + builder.flush(); + return BytesReference.bytes(builder); + } + + private License sign(License license) throws IOException { + final Path publicKey = getDataPath("/public.key"); + final Path privateKey = getDataPath("/private.key"); + final LicenseSigner signer = new LicenseSigner(privateKey, publicKey); + + return signer.sign(license); + } + + private License buildLicense(License.LicenseType type, TimeValue expires) { + return License.builder() + .uid(new UUID(randomLong(), randomLong()).toString()) + .type(type) + .expiryDate(System.currentTimeMillis() + expires.millis()) + .issuer(randomAlphaOfLengthBetween(5, 60)) + .issuedTo(randomAlphaOfLengthBetween(5, 60)) + .issueDate(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(randomLongBetween(1, 5000))) + .maxNodes(randomIntBetween(1, 500)) + .signature(null) + .build(); + } } From 71a0500d27e4dd3a87fc98e996869d491aecfcae Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Tue, 10 Dec 2019 10:22:53 +0200 Subject: [PATCH 135/686] [ML] Introduce randomize_seed setting for regression and classification (#49990) This adds a new `randomize_seed` for regression and classification. When not explicitly set, the seed is randomly generated. One can reuse the seed in a similar job in order to ensure the same docs are picked for training. --- .../client/ml/dataframe/Classification.java | 27 +++++- .../client/ml/dataframe/Regression.java | 29 +++++-- .../client/MachineLearningIT.java | 2 + .../MlClientDocumentationIT.java | 4 +- .../ml/dataframe/ClassificationTests.java | 1 + .../ml/put-data-frame-analytics.asciidoc | 4 +- .../apis/dfanalyticsresources.asciidoc | 4 + .../apis/put-dfanalytics.asciidoc | 4 +- docs/reference/ml/ml-shared.asciidoc | 9 ++ .../dataframe/DataFrameAnalyticsConfig.java | 3 +- .../dataframe/analyses/BoostedTreeParams.java | 4 +- .../ml/dataframe/analyses/Classification.java | 41 +++++++-- .../ml/dataframe/analyses/Regression.java | 41 +++++++-- .../DataFrameAnalyticsConfigTests.java | 47 ++++++++++- .../analyses/ClassificationTests.java | 84 +++++++++++++++---- .../dataframe/analyses/RegressionTests.java | 71 ++++++++++++++-- .../ml/integration/ClassificationIT.java | 50 +++++++++-- ...NativeDataFrameAnalyticsIntegTestCase.java | 22 +++++ .../xpack/ml/integration/RegressionIT.java | 41 ++++++++- .../TransportPutDataFrameAnalyticsAction.java | 12 +-- .../CustomProcessorFactory.java | 4 +- .../DatasetSplittingCustomProcessor.java | 6 +- .../DatasetSplittingCustomProcessorTests.java | 10 ++- .../test/ml/data_frame_analytics_crud.yml | 16 ++-- 24 files changed, 460 insertions(+), 76 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/Classification.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/Classification.java index d4e7bce5ec442..9d384e6d86786 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/Classification.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/Classification.java @@ -49,6 +49,7 @@ public static Builder builder(String dependentVariable) { static final ParseField PREDICTION_FIELD_NAME = new ParseField("prediction_field_name"); static final ParseField TRAINING_PERCENT = new ParseField("training_percent"); static final ParseField NUM_TOP_CLASSES = new ParseField("num_top_classes"); + static final ParseField RANDOMIZE_SEED = new ParseField("randomize_seed"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( @@ -63,7 +64,8 @@ public static Builder builder(String dependentVariable) { (Double) a[5], (String) a[6], (Double) a[7], - (Integer) a[8])); + (Integer) a[8], + (Long) a[9])); static { PARSER.declareString(ConstructingObjectParser.constructorArg(), DEPENDENT_VARIABLE); @@ -75,6 +77,7 @@ public static Builder builder(String dependentVariable) { PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), PREDICTION_FIELD_NAME); PARSER.declareDouble(ConstructingObjectParser.optionalConstructorArg(), TRAINING_PERCENT); PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), NUM_TOP_CLASSES); + PARSER.declareLong(ConstructingObjectParser.optionalConstructorArg(), RANDOMIZE_SEED); } private final String dependentVariable; @@ -86,10 +89,11 @@ public static Builder builder(String dependentVariable) { private final String predictionFieldName; private final Double trainingPercent; private final Integer numTopClasses; + private final Long randomizeSeed; private Classification(String dependentVariable, @Nullable Double lambda, @Nullable Double gamma, @Nullable Double eta, @Nullable Integer maximumNumberTrees, @Nullable Double featureBagFraction, @Nullable String predictionFieldName, - @Nullable Double trainingPercent, @Nullable Integer numTopClasses) { + @Nullable Double trainingPercent, @Nullable Integer numTopClasses, @Nullable Long randomizeSeed) { this.dependentVariable = Objects.requireNonNull(dependentVariable); this.lambda = lambda; this.gamma = gamma; @@ -99,6 +103,7 @@ private Classification(String dependentVariable, @Nullable Double lambda, @Nulla this.predictionFieldName = predictionFieldName; this.trainingPercent = trainingPercent; this.numTopClasses = numTopClasses; + this.randomizeSeed = randomizeSeed; } @Override @@ -138,6 +143,10 @@ public Double getTrainingPercent() { return trainingPercent; } + public Long getRandomizeSeed() { + return randomizeSeed; + } + public Integer getNumTopClasses() { return numTopClasses; } @@ -167,6 +176,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (trainingPercent != null) { builder.field(TRAINING_PERCENT.getPreferredName(), trainingPercent); } + if (randomizeSeed != null) { + builder.field(RANDOMIZE_SEED.getPreferredName(), randomizeSeed); + } if (numTopClasses != null) { builder.field(NUM_TOP_CLASSES.getPreferredName(), numTopClasses); } @@ -177,7 +189,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override public int hashCode() { return Objects.hash(dependentVariable, lambda, gamma, eta, maximumNumberTrees, featureBagFraction, predictionFieldName, - trainingPercent, numTopClasses); + trainingPercent, randomizeSeed, numTopClasses); } @Override @@ -193,6 +205,7 @@ public boolean equals(Object o) { && Objects.equals(featureBagFraction, that.featureBagFraction) && Objects.equals(predictionFieldName, that.predictionFieldName) && Objects.equals(trainingPercent, that.trainingPercent) + && Objects.equals(randomizeSeed, that.randomizeSeed) && Objects.equals(numTopClasses, that.numTopClasses); } @@ -211,6 +224,7 @@ public static class Builder { private String predictionFieldName; private Double trainingPercent; private Integer numTopClasses; + private Long randomizeSeed; private Builder(String dependentVariable) { this.dependentVariable = Objects.requireNonNull(dependentVariable); @@ -251,6 +265,11 @@ public Builder setTrainingPercent(Double trainingPercent) { return this; } + public Builder setRandomizeSeed(Long randomizeSeed) { + this.randomizeSeed = randomizeSeed; + return this; + } + public Builder setNumTopClasses(Integer numTopClasses) { this.numTopClasses = numTopClasses; return this; @@ -258,7 +277,7 @@ public Builder setNumTopClasses(Integer numTopClasses) { public Classification build() { return new Classification(dependentVariable, lambda, gamma, eta, maximumNumberTrees, featureBagFraction, predictionFieldName, - trainingPercent, numTopClasses); + trainingPercent, numTopClasses, randomizeSeed); } } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/Regression.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/Regression.java index 3c1edece6fc16..fa55ee40b27fb 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/Regression.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/Regression.java @@ -48,6 +48,7 @@ public static Builder builder(String dependentVariable) { static final ParseField FEATURE_BAG_FRACTION = new ParseField("feature_bag_fraction"); static final ParseField PREDICTION_FIELD_NAME = new ParseField("prediction_field_name"); static final ParseField TRAINING_PERCENT = new ParseField("training_percent"); + static final ParseField RANDOMIZE_SEED = new ParseField("randomize_seed"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( @@ -61,7 +62,8 @@ public static Builder builder(String dependentVariable) { (Integer) a[4], (Double) a[5], (String) a[6], - (Double) a[7])); + (Double) a[7], + (Long) a[8])); static { PARSER.declareString(ConstructingObjectParser.constructorArg(), DEPENDENT_VARIABLE); @@ -72,6 +74,7 @@ public static Builder builder(String dependentVariable) { PARSER.declareDouble(ConstructingObjectParser.optionalConstructorArg(), FEATURE_BAG_FRACTION); PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), PREDICTION_FIELD_NAME); PARSER.declareDouble(ConstructingObjectParser.optionalConstructorArg(), TRAINING_PERCENT); + PARSER.declareLong(ConstructingObjectParser.optionalConstructorArg(), RANDOMIZE_SEED); } private final String dependentVariable; @@ -82,10 +85,11 @@ public static Builder builder(String dependentVariable) { private final Double featureBagFraction; private final String predictionFieldName; private final Double trainingPercent; + private final Long randomizeSeed; private Regression(String dependentVariable, @Nullable Double lambda, @Nullable Double gamma, @Nullable Double eta, @Nullable Integer maximumNumberTrees, @Nullable Double featureBagFraction, @Nullable String predictionFieldName, - @Nullable Double trainingPercent) { + @Nullable Double trainingPercent, @Nullable Long randomizeSeed) { this.dependentVariable = Objects.requireNonNull(dependentVariable); this.lambda = lambda; this.gamma = gamma; @@ -94,6 +98,7 @@ private Regression(String dependentVariable, @Nullable Double lambda, @Nullable this.featureBagFraction = featureBagFraction; this.predictionFieldName = predictionFieldName; this.trainingPercent = trainingPercent; + this.randomizeSeed = randomizeSeed; } @Override @@ -133,6 +138,10 @@ public Double getTrainingPercent() { return trainingPercent; } + public Long getRandomizeSeed() { + return randomizeSeed; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -158,6 +167,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (trainingPercent != null) { builder.field(TRAINING_PERCENT.getPreferredName(), trainingPercent); } + if (randomizeSeed != null) { + builder.field(RANDOMIZE_SEED.getPreferredName(), randomizeSeed); + } builder.endObject(); return builder; } @@ -165,7 +177,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override public int hashCode() { return Objects.hash(dependentVariable, lambda, gamma, eta, maximumNumberTrees, featureBagFraction, predictionFieldName, - trainingPercent); + trainingPercent, randomizeSeed); } @Override @@ -180,7 +192,8 @@ public boolean equals(Object o) { && Objects.equals(maximumNumberTrees, that.maximumNumberTrees) && Objects.equals(featureBagFraction, that.featureBagFraction) && Objects.equals(predictionFieldName, that.predictionFieldName) - && Objects.equals(trainingPercent, that.trainingPercent); + && Objects.equals(trainingPercent, that.trainingPercent) + && Objects.equals(randomizeSeed, that.randomizeSeed); } @Override @@ -197,6 +210,7 @@ public static class Builder { private Double featureBagFraction; private String predictionFieldName; private Double trainingPercent; + private Long randomizeSeed; private Builder(String dependentVariable) { this.dependentVariable = Objects.requireNonNull(dependentVariable); @@ -237,9 +251,14 @@ public Builder setTrainingPercent(Double trainingPercent) { return this; } + public Builder setRandomizeSeed(Long randomizeSeed) { + this.randomizeSeed = randomizeSeed; + return this; + } + public Regression build() { return new Regression(dependentVariable, lambda, gamma, eta, maximumNumberTrees, featureBagFraction, predictionFieldName, - trainingPercent); + trainingPercent, randomizeSeed); } } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index 6ed3734831aa2..29e69c5095cbd 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -1291,6 +1291,7 @@ public void testPutDataFrameAnalyticsConfig_GivenRegression() throws Exception { .setAnalysis(org.elasticsearch.client.ml.dataframe.Regression.builder("my_dependent_variable") .setPredictionFieldName("my_dependent_variable_prediction") .setTrainingPercent(80.0) + .setRandomizeSeed(42L) .build()) .setDescription("this is a regression") .build(); @@ -1326,6 +1327,7 @@ public void testPutDataFrameAnalyticsConfig_GivenClassification() throws Excepti .setAnalysis(org.elasticsearch.client.ml.dataframe.Classification.builder("my_dependent_variable") .setPredictionFieldName("my_dependent_variable_prediction") .setTrainingPercent(80.0) + .setRandomizeSeed(42L) .setNumTopClasses(1) .build()) .setDescription("this is a classification") diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 1d9a151cf8ae3..13185e221633b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -2975,7 +2975,8 @@ public void testPutDataFrameAnalytics() throws Exception { .setFeatureBagFraction(0.4) // <6> .setPredictionFieldName("my_prediction_field_name") // <7> .setTrainingPercent(50.0) // <8> - .setNumTopClasses(1) // <9> + .setRandomizeSeed(1234L) // <9> + .setNumTopClasses(1) // <10> .build(); // end::put-data-frame-analytics-classification @@ -2988,6 +2989,7 @@ public void testPutDataFrameAnalytics() throws Exception { .setFeatureBagFraction(0.4) // <6> .setPredictionFieldName("my_prediction_field_name") // <7> .setTrainingPercent(50.0) // <8> + .setRandomizeSeed(1234L) // <9> .build(); // end::put-data-frame-analytics-regression diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/ClassificationTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/ClassificationTests.java index 98f060cc8534a..5ef8fdaef5a27 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/ClassificationTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/ClassificationTests.java @@ -34,6 +34,7 @@ public static Classification randomClassification() { .setFeatureBagFraction(randomBoolean() ? null : randomDoubleBetween(0.0, 1.0, false)) .setPredictionFieldName(randomBoolean() ? null : randomAlphaOfLength(10)) .setTrainingPercent(randomBoolean() ? null : randomDoubleBetween(1.0, 100.0, true)) + .setRandomizeSeed(randomBoolean() ? null : randomLong()) .setNumTopClasses(randomBoolean() ? null : randomIntBetween(0, 10)) .build(); } diff --git a/docs/java-rest/high-level/ml/put-data-frame-analytics.asciidoc b/docs/java-rest/high-level/ml/put-data-frame-analytics.asciidoc index 91a97ad604cee..2152eff5c0850 100644 --- a/docs/java-rest/high-level/ml/put-data-frame-analytics.asciidoc +++ b/docs/java-rest/high-level/ml/put-data-frame-analytics.asciidoc @@ -119,7 +119,8 @@ include-tagged::{doc-tests-file}[{api}-classification] <6> The fraction of features which will be used when selecting a random bag for each candidate split. A double in (0, 1]. <7> The name of the prediction field in the results object. <8> The percentage of training-eligible rows to be used in training. Defaults to 100%. -<9> The number of top classes to be reported in the results. Defaults to 2. +<9> The seed to be used by the random generator that picks which rows are used in training. +<10> The number of top classes to be reported in the results. Defaults to 2. ===== Regression @@ -138,6 +139,7 @@ include-tagged::{doc-tests-file}[{api}-regression] <6> The fraction of features which will be used when selecting a random bag for each candidate split. A double in (0, 1]. <7> The name of the prediction field in the results object. <8> The percentage of training-eligible rows to be used in training. Defaults to 100%. +<9> The seed to be used by the random generator that picks which rows are used in training. ==== Analyzed fields diff --git a/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc b/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc index e8ee463c66af7..111953b8321ab 100644 --- a/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc +++ b/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc @@ -204,6 +204,8 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=prediction_field_name] include::{docdir}/ml/ml-shared.asciidoc[tag=training_percent] +include::{docdir}/ml/ml-shared.asciidoc[tag=randomize_seed] + [float] [[regression-resources-advanced]] @@ -252,6 +254,8 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=prediction_field_name] include::{docdir}/ml/ml-shared.asciidoc[tag=training_percent] +include::{docdir}/ml/ml-shared.asciidoc[tag=randomize_seed] + [float] [[classification-resources-advanced]] diff --git a/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc index 5b0987e41c4bc..123eb6633e37b 100644 --- a/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc @@ -397,7 +397,8 @@ PUT _ml/data_frame/analytics/student_performance_mathematics_0.3 { "regression": { "dependent_variable": "G3", - "training_percent": 70 <1> + "training_percent": 70, <1> + "randomize_seed": 19673948271 <2> } } } @@ -406,6 +407,7 @@ PUT _ml/data_frame/analytics/student_performance_mathematics_0.3 <1> The `training_percent` defines the percentage of the data set that will be used for training the model. +<2> The `randomize_seed` is the seed used to randomly pick which data is used for training. [[ml-put-dfanalytics-example-c]] diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index 11e062796afa6..bea970078d06b 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -681,6 +681,15 @@ those that contain arrays) won’t be included in the calculation for used percentage. Defaults to `100`. end::training_percent[] +tag::randomize_seed[] +`randomize_seed`:: +(Optional, long) Defines the seed to the random generator that is used to pick +which documents will be used for training. By default it is randomly generated. +Set it to a specific value to ensure the same documents are used for training +assuming other related parameters (e.g. `source`, `analyzed_fields`, etc.) are the same. +end::randomize_seed[] + + tag::use-null[] Defines whether a new series is used as the null series when there is no value for the by or partition fields. The default value is `false`. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java index 9fd7f8aa86fcb..1142b5411fb0c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java @@ -225,7 +225,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(DEST.getPreferredName(), dest); builder.startObject(ANALYSIS.getPreferredName()); - builder.field(analysis.getWriteableName(), analysis); + builder.field(analysis.getWriteableName(), analysis, + new MapParams(Collections.singletonMap(VERSION.getPreferredName(), version == null ? null : version.toString()))); builder.endObject(); if (params.paramAsBoolean(ToXContentParams.FOR_INTERNAL_STORAGE, false)) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/BoostedTreeParams.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/BoostedTreeParams.java index ed3cff7d73c0c..0f06b08444f53 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/BoostedTreeParams.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/BoostedTreeParams.java @@ -49,7 +49,7 @@ static void declareFields(AbstractObjectParser parser) { private final Integer maximumNumberTrees; private final Double featureBagFraction; - BoostedTreeParams(@Nullable Double lambda, + public BoostedTreeParams(@Nullable Double lambda, @Nullable Double gamma, @Nullable Double eta, @Nullable Integer maximumNumberTrees, @@ -76,7 +76,7 @@ static void declareFields(AbstractObjectParser parser) { this.featureBagFraction = featureBagFraction; } - BoostedTreeParams() { + public BoostedTreeParams() { this(null, null, null, null, null); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java index b4b258ea161fa..cd96b815fc11e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java @@ -5,8 +5,10 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.analyses; +import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Randomness; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -35,6 +37,7 @@ public class Classification implements DataFrameAnalysis { public static final ParseField PREDICTION_FIELD_NAME = new ParseField("prediction_field_name"); public static final ParseField NUM_TOP_CLASSES = new ParseField("num_top_classes"); public static final ParseField TRAINING_PERCENT = new ParseField("training_percent"); + public static final ParseField RANDOMIZE_SEED = new ParseField("randomize_seed"); private static final ConstructingObjectParser LENIENT_PARSER = createParser(true); private static final ConstructingObjectParser STRICT_PARSER = createParser(false); @@ -48,12 +51,14 @@ private static ConstructingObjectParser createParser(boole new BoostedTreeParams((Double) a[1], (Double) a[2], (Double) a[3], (Integer) a[4], (Double) a[5]), (String) a[6], (Integer) a[7], - (Double) a[8])); + (Double) a[8], + (Long) a[9])); parser.declareString(constructorArg(), DEPENDENT_VARIABLE); BoostedTreeParams.declareFields(parser); parser.declareString(optionalConstructorArg(), PREDICTION_FIELD_NAME); parser.declareInt(optionalConstructorArg(), NUM_TOP_CLASSES); parser.declareDouble(optionalConstructorArg(), TRAINING_PERCENT); + parser.declareLong(optionalConstructorArg(), RANDOMIZE_SEED); return parser; } @@ -82,12 +87,14 @@ public static Classification fromXContent(XContentParser parser, boolean ignoreU private final String predictionFieldName; private final int numTopClasses; private final double trainingPercent; + private final long randomizeSeed; public Classification(String dependentVariable, BoostedTreeParams boostedTreeParams, @Nullable String predictionFieldName, @Nullable Integer numTopClasses, - @Nullable Double trainingPercent) { + @Nullable Double trainingPercent, + @Nullable Long randomizeSeed) { if (numTopClasses != null && (numTopClasses < 0 || numTopClasses > 1000)) { throw ExceptionsHelper.badRequestException("[{}] must be an integer in [0, 1000]", NUM_TOP_CLASSES.getPreferredName()); } @@ -99,10 +106,11 @@ public Classification(String dependentVariable, this.predictionFieldName = predictionFieldName == null ? dependentVariable + "_prediction" : predictionFieldName; this.numTopClasses = numTopClasses == null ? DEFAULT_NUM_TOP_CLASSES : numTopClasses; this.trainingPercent = trainingPercent == null ? 100.0 : trainingPercent; + this.randomizeSeed = randomizeSeed == null ? Randomness.get().nextLong() : randomizeSeed; } public Classification(String dependentVariable) { - this(dependentVariable, new BoostedTreeParams(), null, null, null); + this(dependentVariable, new BoostedTreeParams(), null, null, null, null); } public Classification(StreamInput in) throws IOException { @@ -111,12 +119,21 @@ public Classification(StreamInput in) throws IOException { predictionFieldName = in.readOptionalString(); numTopClasses = in.readOptionalVInt(); trainingPercent = in.readDouble(); + if (in.getVersion().onOrAfter(Version.CURRENT)) { + randomizeSeed = in.readOptionalLong(); + } else { + randomizeSeed = Randomness.get().nextLong(); + } } public String getDependentVariable() { return dependentVariable; } + public BoostedTreeParams getBoostedTreeParams() { + return boostedTreeParams; + } + public String getPredictionFieldName() { return predictionFieldName; } @@ -129,6 +146,11 @@ public double getTrainingPercent() { return trainingPercent; } + @Nullable + public Long getRandomizeSeed() { + return randomizeSeed; + } + @Override public String getWriteableName() { return NAME.getPreferredName(); @@ -141,10 +163,15 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(predictionFieldName); out.writeOptionalVInt(numTopClasses); out.writeDouble(trainingPercent); + if (out.getVersion().onOrAfter(Version.CURRENT)) { + out.writeOptionalLong(randomizeSeed); + } } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + Version version = Version.fromString(params.param("version", Version.CURRENT.toString())); + builder.startObject(); builder.field(DEPENDENT_VARIABLE.getPreferredName(), dependentVariable); boostedTreeParams.toXContent(builder, params); @@ -153,6 +180,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(PREDICTION_FIELD_NAME.getPreferredName(), predictionFieldName); } builder.field(TRAINING_PERCENT.getPreferredName(), trainingPercent); + if (version.onOrAfter(Version.CURRENT)) { + builder.field(RANDOMIZE_SEED.getPreferredName(), randomizeSeed); + } builder.endObject(); return builder; } @@ -238,11 +268,12 @@ public boolean equals(Object o) { && Objects.equals(boostedTreeParams, that.boostedTreeParams) && Objects.equals(predictionFieldName, that.predictionFieldName) && Objects.equals(numTopClasses, that.numTopClasses) - && trainingPercent == that.trainingPercent; + && trainingPercent == that.trainingPercent + && randomizeSeed == that.randomizeSeed; } @Override public int hashCode() { - return Objects.hash(dependentVariable, boostedTreeParams, predictionFieldName, numTopClasses, trainingPercent); + return Objects.hash(dependentVariable, boostedTreeParams, predictionFieldName, numTopClasses, trainingPercent, randomizeSeed); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java index 01388f01d807c..dd8f6a91272c2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java @@ -5,8 +5,10 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.analyses; +import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Randomness; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -32,6 +34,7 @@ public class Regression implements DataFrameAnalysis { public static final ParseField DEPENDENT_VARIABLE = new ParseField("dependent_variable"); public static final ParseField PREDICTION_FIELD_NAME = new ParseField("prediction_field_name"); public static final ParseField TRAINING_PERCENT = new ParseField("training_percent"); + public static final ParseField RANDOMIZE_SEED = new ParseField("randomize_seed"); private static final ConstructingObjectParser LENIENT_PARSER = createParser(true); private static final ConstructingObjectParser STRICT_PARSER = createParser(false); @@ -44,11 +47,13 @@ private static ConstructingObjectParser createParser(boolean l (String) a[0], new BoostedTreeParams((Double) a[1], (Double) a[2], (Double) a[3], (Integer) a[4], (Double) a[5]), (String) a[6], - (Double) a[7])); + (Double) a[7], + (Long) a[8])); parser.declareString(constructorArg(), DEPENDENT_VARIABLE); BoostedTreeParams.declareFields(parser); parser.declareString(optionalConstructorArg(), PREDICTION_FIELD_NAME); parser.declareDouble(optionalConstructorArg(), TRAINING_PERCENT); + parser.declareLong(optionalConstructorArg(), RANDOMIZE_SEED); return parser; } @@ -60,11 +65,13 @@ public static Regression fromXContent(XContentParser parser, boolean ignoreUnkno private final BoostedTreeParams boostedTreeParams; private final String predictionFieldName; private final double trainingPercent; + private final long randomizeSeed; public Regression(String dependentVariable, BoostedTreeParams boostedTreeParams, @Nullable String predictionFieldName, - @Nullable Double trainingPercent) { + @Nullable Double trainingPercent, + @Nullable Long randomizeSeed) { if (trainingPercent != null && (trainingPercent < 1.0 || trainingPercent > 100.0)) { throw ExceptionsHelper.badRequestException("[{}] must be a double in [1, 100]", TRAINING_PERCENT.getPreferredName()); } @@ -72,10 +79,11 @@ public Regression(String dependentVariable, this.boostedTreeParams = ExceptionsHelper.requireNonNull(boostedTreeParams, BoostedTreeParams.NAME); this.predictionFieldName = predictionFieldName == null ? dependentVariable + "_prediction" : predictionFieldName; this.trainingPercent = trainingPercent == null ? 100.0 : trainingPercent; + this.randomizeSeed = randomizeSeed == null ? Randomness.get().nextLong() : randomizeSeed; } public Regression(String dependentVariable) { - this(dependentVariable, new BoostedTreeParams(), null, null); + this(dependentVariable, new BoostedTreeParams(), null, null, null); } public Regression(StreamInput in) throws IOException { @@ -83,12 +91,21 @@ public Regression(StreamInput in) throws IOException { boostedTreeParams = new BoostedTreeParams(in); predictionFieldName = in.readOptionalString(); trainingPercent = in.readDouble(); + if (in.getVersion().onOrAfter(Version.CURRENT)) { + randomizeSeed = in.readOptionalLong(); + } else { + randomizeSeed = Randomness.get().nextLong(); + } } public String getDependentVariable() { return dependentVariable; } + public BoostedTreeParams getBoostedTreeParams() { + return boostedTreeParams; + } + public String getPredictionFieldName() { return predictionFieldName; } @@ -97,6 +114,11 @@ public double getTrainingPercent() { return trainingPercent; } + @Nullable + public Long getRandomizeSeed() { + return randomizeSeed; + } + @Override public String getWriteableName() { return NAME.getPreferredName(); @@ -108,10 +130,15 @@ public void writeTo(StreamOutput out) throws IOException { boostedTreeParams.writeTo(out); out.writeOptionalString(predictionFieldName); out.writeDouble(trainingPercent); + if (out.getVersion().onOrAfter(Version.CURRENT)) { + out.writeOptionalLong(randomizeSeed); + } } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + Version version = Version.fromString(params.param("version", Version.CURRENT.toString())); + builder.startObject(); builder.field(DEPENDENT_VARIABLE.getPreferredName(), dependentVariable); boostedTreeParams.toXContent(builder, params); @@ -119,6 +146,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(PREDICTION_FIELD_NAME.getPreferredName(), predictionFieldName); } builder.field(TRAINING_PERCENT.getPreferredName(), trainingPercent); + if (version.onOrAfter(Version.CURRENT)) { + builder.field(RANDOMIZE_SEED.getPreferredName(), randomizeSeed); + } builder.endObject(); return builder; } @@ -177,11 +207,12 @@ public boolean equals(Object o) { return Objects.equals(dependentVariable, that.dependentVariable) && Objects.equals(boostedTreeParams, that.boostedTreeParams) && Objects.equals(predictionFieldName, that.predictionFieldName) - && trainingPercent == that.trainingPercent; + && trainingPercent == that.trainingPercent + && randomizeSeed == randomizeSeed; } @Override public int hashCode() { - return Objects.hash(dependentVariable, boostedTreeParams, predictionFieldName, trainingPercent); + return Objects.hash(dependentVariable, boostedTreeParams, predictionFieldName, trainingPercent, randomizeSeed); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfigTests.java index d6b2c077388e3..880bea8884658 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfigTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.Version; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.Writeable; @@ -20,17 +21,20 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParseException; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.dataframe.analyses.MlDataFrameAnalysisNamedXContentProvider; import org.elasticsearch.xpack.core.ml.dataframe.analyses.OutlierDetectionTests; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.Regression; import org.elasticsearch.xpack.core.ml.utils.ToXContentParams; import org.junit.Before; @@ -42,10 +46,13 @@ import java.util.List; import java.util.Map; -import static org.hamcrest.Matchers.equalTo; 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.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.startsWith; public class DataFrameAnalyticsConfigTests extends AbstractSerializingTestCase { @@ -339,6 +346,44 @@ public void testPreventVersionInjection() throws IOException { } } + public void testToXContent_GivenAnalysisWithRandomizeSeedAndVersionIsCurrent() throws IOException { + Regression regression = new Regression("foo"); + assertThat(regression.getRandomizeSeed(), is(notNullValue())); + + DataFrameAnalyticsConfig config = new DataFrameAnalyticsConfig.Builder() + .setVersion(Version.CURRENT) + .setId("test_config") + .setSource(new DataFrameAnalyticsSource(new String[] {"source_index"}, null, null)) + .setDest(new DataFrameAnalyticsDest("dest_index", null)) + .setAnalysis(regression) + .build(); + + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + config.toXContent(builder, ToXContent.EMPTY_PARAMS); + String json = Strings.toString(builder); + assertThat(json, containsString("randomize_seed")); + } + } + + public void testToXContent_GivenAnalysisWithRandomizeSeedAndVersionIsBeforeItWasIntroduced() throws IOException { + Regression regression = new Regression("foo"); + assertThat(regression.getRandomizeSeed(), is(notNullValue())); + + DataFrameAnalyticsConfig config = new DataFrameAnalyticsConfig.Builder() + .setVersion(Version.V_7_5_0) + .setId("test_config") + .setSource(new DataFrameAnalyticsSource(new String[] {"source_index"}, null, null)) + .setDest(new DataFrameAnalyticsDest("dest_index", null)) + .setAnalysis(regression) + .build(); + + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + config.toXContent(builder, ToXContent.EMPTY_PARAMS); + String json = Strings.toString(builder); + assertThat(json, not(containsString("randomize_seed"))); + } + } + private static void assertTooSmall(ElasticsearchStatusException e) { assertThat(e.getMessage(), startsWith("model_memory_limit must be at least 1kb.")); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java index 61d6b4dfe3f7a..8308ef8dad289 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java @@ -6,20 +6,28 @@ package org.elasticsearch.xpack.core.ml.dataframe.analyses; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.Version; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.mapper.BooleanFieldMapper; import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.NumberFieldMapper; import org.elasticsearch.test.AbstractSerializingTestCase; import java.io.IOException; +import java.util.Collections; import java.util.Map; import java.util.Set; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; public class ClassificationTests extends AbstractSerializingTestCase { @@ -42,7 +50,9 @@ public static Classification createRandom() { String predictionFieldName = randomBoolean() ? null : randomAlphaOfLength(10); Integer numTopClasses = randomBoolean() ? null : randomIntBetween(0, 1000); Double trainingPercent = randomBoolean() ? null : randomDoubleBetween(1.0, 100.0, true); - return new Classification(dependentVariableName, boostedTreeParams, predictionFieldName, numTopClasses, trainingPercent); + Long randomizeSeed = randomBoolean() ? null : randomLong(); + return new Classification(dependentVariableName, boostedTreeParams, predictionFieldName, numTopClasses, trainingPercent, + randomizeSeed); } @Override @@ -52,71 +62,71 @@ protected Writeable.Reader instanceReader() { public void testConstructor_GivenTrainingPercentIsLessThanOne() { ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 0.999)); + () -> new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 0.999, randomLong())); assertThat(e.getMessage(), equalTo("[training_percent] must be a double in [1, 100]")); } public void testConstructor_GivenTrainingPercentIsGreaterThan100() { ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 100.0001)); + () -> new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 100.0001, randomLong())); assertThat(e.getMessage(), equalTo("[training_percent] must be a double in [1, 100]")); } public void testConstructor_GivenNumTopClassesIsLessThanZero() { ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> new Classification("foo", BOOSTED_TREE_PARAMS, "result", -1, 1.0)); + () -> new Classification("foo", BOOSTED_TREE_PARAMS, "result", -1, 1.0, randomLong())); assertThat(e.getMessage(), equalTo("[num_top_classes] must be an integer in [0, 1000]")); } public void testConstructor_GivenNumTopClassesIsGreaterThan1000() { ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> new Classification("foo", BOOSTED_TREE_PARAMS, "result", 1001, 1.0)); + () -> new Classification("foo", BOOSTED_TREE_PARAMS, "result", 1001, 1.0, randomLong())); assertThat(e.getMessage(), equalTo("[num_top_classes] must be an integer in [0, 1000]")); } public void testGetPredictionFieldName() { - Classification classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 50.0); + Classification classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 50.0, randomLong()); assertThat(classification.getPredictionFieldName(), equalTo("result")); - classification = new Classification("foo", BOOSTED_TREE_PARAMS, null, 3, 50.0); + classification = new Classification("foo", BOOSTED_TREE_PARAMS, null, 3, 50.0, randomLong()); assertThat(classification.getPredictionFieldName(), equalTo("foo_prediction")); } public void testGetNumTopClasses() { - Classification classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 7, 1.0); + Classification classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 7, 1.0, randomLong()); assertThat(classification.getNumTopClasses(), equalTo(7)); // Boundary condition: num_top_classes == 0 - classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 0, 1.0); + classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 0, 1.0, randomLong()); assertThat(classification.getNumTopClasses(), equalTo(0)); // Boundary condition: num_top_classes == 1000 - classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 1000, 1.0); + classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 1000, 1.0, randomLong()); assertThat(classification.getNumTopClasses(), equalTo(1000)); // num_top_classes == null, default applied - classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", null, 1.0); + classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", null, 1.0, randomLong()); assertThat(classification.getNumTopClasses(), equalTo(2)); } public void testGetTrainingPercent() { - Classification classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 50.0); + Classification classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 50.0, randomLong()); assertThat(classification.getTrainingPercent(), equalTo(50.0)); // Boundary condition: training_percent == 1.0 - classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 1.0); + classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 1.0, randomLong()); assertThat(classification.getTrainingPercent(), equalTo(1.0)); // Boundary condition: training_percent == 100.0 - classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 100.0); + classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, 100.0, randomLong()); assertThat(classification.getTrainingPercent(), equalTo(100.0)); // training_percent == null, default applied - classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, null); + classification = new Classification("foo", BOOSTED_TREE_PARAMS, "result", 3, null, randomLong()); assertThat(classification.getTrainingPercent(), equalTo(100.0)); } @@ -155,4 +165,48 @@ public void testGetParams() { public void testFieldCardinalityLimitsIsNonNull() { assertThat(createTestInstance().getFieldCardinalityLimits(), is(not(nullValue()))); } + + public void testToXContent_GivenVersionBeforeRandomizeSeedWasIntroduced() throws IOException { + Classification classification = createRandom(); + assertThat(classification.getRandomizeSeed(), is(notNullValue())); + + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + classification.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("version", "7.5.0"))); + String json = Strings.toString(builder); + assertThat(json, not(containsString("randomize_seed"))); + } + } + + public void testToXContent_GivenVersionAfterRandomizeSeedWasIntroduced() throws IOException { + Classification classification = createRandom(); + assertThat(classification.getRandomizeSeed(), is(notNullValue())); + + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + classification.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("version", Version.CURRENT.toString()))); + String json = Strings.toString(builder); + assertThat(json, containsString("randomize_seed")); + } + } + + public void testToXContent_GivenVersionIsNull() throws IOException { + Classification classification = createRandom(); + assertThat(classification.getRandomizeSeed(), is(notNullValue())); + + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + classification.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("version", null))); + String json = Strings.toString(builder); + assertThat(json, containsString("randomize_seed")); + } + } + + public void testToXContent_GivenEmptyParams() throws IOException { + Classification classification = createRandom(); + assertThat(classification.getRandomizeSeed(), is(notNullValue())); + + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + classification.toXContent(builder, ToXContent.EMPTY_PARAMS); + String json = Strings.toString(builder); + assertThat(json, containsString("randomize_seed")); + } + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java index f3d5312280e88..58e19f6ef6a2a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java @@ -6,16 +6,24 @@ package org.elasticsearch.xpack.core.ml.dataframe.analyses; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.Version; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.AbstractSerializingTestCase; import java.io.IOException; +import java.util.Collections; import java.util.Map; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; public class RegressionTests extends AbstractSerializingTestCase { @@ -37,7 +45,8 @@ public static Regression createRandom() { BoostedTreeParams boostedTreeParams = BoostedTreeParamsTests.createRandom(); String predictionFieldName = randomBoolean() ? null : randomAlphaOfLength(10); Double trainingPercent = randomBoolean() ? null : randomDoubleBetween(1.0, 100.0, true); - return new Regression(dependentVariableName, boostedTreeParams, predictionFieldName, trainingPercent); + Long randomizeSeed = randomBoolean() ? null : randomLong(); + return new Regression(dependentVariableName, boostedTreeParams, predictionFieldName, trainingPercent, randomizeSeed); } @Override @@ -47,40 +56,40 @@ protected Writeable.Reader instanceReader() { public void testConstructor_GivenTrainingPercentIsLessThanOne() { ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> new Regression("foo", BOOSTED_TREE_PARAMS, "result", 0.999)); + () -> new Regression("foo", BOOSTED_TREE_PARAMS, "result", 0.999, randomLong())); assertThat(e.getMessage(), equalTo("[training_percent] must be a double in [1, 100]")); } public void testConstructor_GivenTrainingPercentIsGreaterThan100() { ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> new Regression("foo", BOOSTED_TREE_PARAMS, "result", 100.0001)); + () -> new Regression("foo", BOOSTED_TREE_PARAMS, "result", 100.0001, randomLong())); assertThat(e.getMessage(), equalTo("[training_percent] must be a double in [1, 100]")); } public void testGetPredictionFieldName() { - Regression regression = new Regression("foo", BOOSTED_TREE_PARAMS, "result", 50.0); + Regression regression = new Regression("foo", BOOSTED_TREE_PARAMS, "result", 50.0, randomLong()); assertThat(regression.getPredictionFieldName(), equalTo("result")); - regression = new Regression("foo", BOOSTED_TREE_PARAMS, null, 50.0); + regression = new Regression("foo", BOOSTED_TREE_PARAMS, null, 50.0, randomLong()); assertThat(regression.getPredictionFieldName(), equalTo("foo_prediction")); } public void testGetTrainingPercent() { - Regression regression = new Regression("foo", BOOSTED_TREE_PARAMS, "result", 50.0); + Regression regression = new Regression("foo", BOOSTED_TREE_PARAMS, "result", 50.0, randomLong()); assertThat(regression.getTrainingPercent(), equalTo(50.0)); // Boundary condition: training_percent == 1.0 - regression = new Regression("foo", BOOSTED_TREE_PARAMS, "result", 1.0); + regression = new Regression("foo", BOOSTED_TREE_PARAMS, "result", 1.0, randomLong()); assertThat(regression.getTrainingPercent(), equalTo(1.0)); // Boundary condition: training_percent == 100.0 - regression = new Regression("foo", BOOSTED_TREE_PARAMS, "result", 100.0); + regression = new Regression("foo", BOOSTED_TREE_PARAMS, "result", 100.0, randomLong()); assertThat(regression.getTrainingPercent(), equalTo(100.0)); // training_percent == null, default applied - regression = new Regression("foo", BOOSTED_TREE_PARAMS, "result", null); + regression = new Regression("foo", BOOSTED_TREE_PARAMS, "result", null, randomLong()); assertThat(regression.getTrainingPercent(), equalTo(100.0)); } @@ -100,4 +109,48 @@ public void testGetStateDocId() { String randomId = randomAlphaOfLength(10); assertThat(regression.getStateDocId(randomId), equalTo(randomId + "_regression_state#1")); } + + public void testToXContent_GivenVersionBeforeRandomizeSeedWasIntroduced() throws IOException { + Regression regression = createRandom(); + assertThat(regression.getRandomizeSeed(), is(notNullValue())); + + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + regression.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("version", "7.5.0"))); + String json = Strings.toString(builder); + assertThat(json, not(containsString("randomize_seed"))); + } + } + + public void testToXContent_GivenVersionAfterRandomizeSeedWasIntroduced() throws IOException { + Regression regression = createRandom(); + assertThat(regression.getRandomizeSeed(), is(notNullValue())); + + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + regression.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("version", Version.CURRENT.toString()))); + String json = Strings.toString(builder); + assertThat(json, containsString("randomize_seed")); + } + } + + public void testToXContent_GivenVersionIsNull() throws IOException { + Regression regression = createRandom(); + assertThat(regression.getRandomizeSeed(), is(notNullValue())); + + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + regression.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("version", null))); + String json = Strings.toString(builder); + assertThat(json, containsString("randomize_seed")); + } + } + + public void testToXContent_GivenEmptyParams() throws IOException { + Regression regression = createRandom(); + assertThat(regression.getRandomizeSeed(), is(notNullValue())); + + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + regression.toXContent(builder, ToXContent.EMPTY_PARAMS); + String json = Strings.toString(builder); + assertThat(json, containsString("randomize_seed")); + } + } } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java index f5db9ae690a96..e7c0ccd0e0554 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java @@ -20,6 +20,7 @@ import org.elasticsearch.search.SearchHit; import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.BoostedTreeParams; import org.elasticsearch.xpack.core.ml.dataframe.analyses.BoostedTreeParamsTests; import org.elasticsearch.xpack.core.ml.dataframe.analyses.Classification; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy; @@ -31,6 +32,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import static java.util.stream.Collectors.toList; import static org.hamcrest.Matchers.allOf; @@ -158,7 +160,7 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty( sourceIndex, destIndex, null, - new Classification(dependentVariable, BoostedTreeParamsTests.createRandom(), null, numTopClasses, 50.0)); + new Classification(dependentVariable, BoostedTreeParamsTests.createRandom(), null, numTopClasses, 50.0, null)); registerAnalytics(config); putAnalytics(config); @@ -269,6 +271,44 @@ public void testDependentVariableCardinalityTooHighButWithQueryMakesItWithinRang assertProgress(jobId, 100, 100, 100, 100); } + public void testTwoJobsWithSameRandomizeSeedUseSameTrainingSet() throws Exception { + String sourceIndex = "classification_two_jobs_with_same_randomize_seed_source"; + String dependentVariable = KEYWORD_FIELD; + indexData(sourceIndex, 10, 0, dependentVariable); + + String firstJobId = "classification_two_jobs_with_same_randomize_seed_1"; + String firstJobDestIndex = firstJobId + "_dest"; + + BoostedTreeParams boostedTreeParams = new BoostedTreeParams(1.0, 1.0, 1.0, 1, 1.0); + + DataFrameAnalyticsConfig firstJob = buildAnalytics(firstJobId, sourceIndex, firstJobDestIndex, null, + new Classification(dependentVariable, boostedTreeParams, null, 1, 50.0, null)); + registerAnalytics(firstJob); + putAnalytics(firstJob); + + String secondJobId = "classification_two_jobs_with_same_randomize_seed_2"; + String secondJobDestIndex = secondJobId + "_dest"; + + long randomizeSeed = ((Classification) firstJob.getAnalysis()).getRandomizeSeed(); + DataFrameAnalyticsConfig secondJob = buildAnalytics(secondJobId, sourceIndex, secondJobDestIndex, null, + new Classification(dependentVariable, boostedTreeParams, null, 1, 50.0, randomizeSeed)); + + registerAnalytics(secondJob); + putAnalytics(secondJob); + + // Let's run both jobs in parallel and wait until they are finished + startAnalytics(firstJobId); + startAnalytics(secondJobId); + waitUntilAnalyticsIsStopped(firstJobId); + waitUntilAnalyticsIsStopped(secondJobId); + + // Now we compare they both used the same training rows + Set firstRunTrainingRowsIds = getTrainingRowsIds(firstJobDestIndex); + Set secondRunTrainingRowsIds = getTrainingRowsIds(secondJobDestIndex); + + assertThat(secondRunTrainingRowsIds, equalTo(firstRunTrainingRowsIds)); + } + private void initialize(String jobId) { this.jobId = jobId; this.sourceIndex = jobId + "_source_index"; @@ -340,10 +380,10 @@ private static Map getMlResultsObjectFromDestDoc(Map void assertTopClasses( - Map resultsObject, - int numTopClasses, - String dependentVariable, - List dependentVariableValues) { + Map resultsObject, + int numTopClasses, + String dependentVariable, + List dependentVariableValues) { assertThat(resultsObject.containsKey("top_classes"), is(true)); List> topClasses = (List>) resultsObject.get("top_classes"); assertThat(topClasses, hasSize(numTopClasses)); diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java index 29ef54d3f7524..99223247d7305 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.xpack.core.ml.action.DeleteDataFrameAnalyticsAction; import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction; @@ -45,7 +46,10 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -252,4 +256,22 @@ private static List fetchAllAuditMessages(String dataFrameAnalyticsId) { .map(hit -> (String) hit.getSourceAsMap().get("message")) .collect(Collectors.toList()); } + + protected static Set getTrainingRowsIds(String index) { + Set trainingRowsIds = new HashSet<>(); + SearchResponse hits = client().prepareSearch(index).get(); + for (SearchHit hit : hits.getHits()) { + Map sourceAsMap = hit.getSourceAsMap(); + assertThat(sourceAsMap.containsKey("ml"), is(true)); + @SuppressWarnings("unchecked") + Map resultsObject = (Map) sourceAsMap.get("ml"); + + assertThat(resultsObject.containsKey("is_training"), is(true)); + if (Boolean.TRUE.equals(resultsObject.get("is_training"))) { + trainingRowsIds.add(hit.getId()); + } + } + assertThat(trainingRowsIds.isEmpty(), is(false)); + return trainingRowsIds; + } } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java index 71ea840c53ea8..84d408daacc61 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java @@ -16,6 +16,7 @@ import org.elasticsearch.search.SearchHit; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsState; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.BoostedTreeParams; import org.elasticsearch.xpack.core.ml.dataframe.analyses.BoostedTreeParamsTests; import org.elasticsearch.xpack.core.ml.dataframe.analyses.Regression; import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; @@ -25,6 +26,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; @@ -139,7 +141,7 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty() throws Exception sourceIndex, destIndex, null, - new Regression(DEPENDENT_VARIABLE_FIELD, BoostedTreeParamsTests.createRandom(), null, 50.0)); + new Regression(DEPENDENT_VARIABLE_FIELD, BoostedTreeParamsTests.createRandom(), null, 50.0, null)); registerAnalytics(config); putAnalytics(config); @@ -235,6 +237,43 @@ public void testStopAndRestart() throws Exception { assertInferenceModelPersisted(jobId); } + public void testTwoJobsWithSameRandomizeSeedUseSameTrainingSet() throws Exception { + String sourceIndex = "regression_two_jobs_with_same_randomize_seed_source"; + indexData(sourceIndex, 10, 0); + + String firstJobId = "regression_two_jobs_with_same_randomize_seed_1"; + String firstJobDestIndex = firstJobId + "_dest"; + + BoostedTreeParams boostedTreeParams = new BoostedTreeParams(1.0, 1.0, 1.0, 1, 1.0); + + DataFrameAnalyticsConfig firstJob = buildAnalytics(firstJobId, sourceIndex, firstJobDestIndex, null, + new Regression(DEPENDENT_VARIABLE_FIELD, boostedTreeParams, null, 50.0, null)); + registerAnalytics(firstJob); + putAnalytics(firstJob); + + String secondJobId = "regression_two_jobs_with_same_randomize_seed_2"; + String secondJobDestIndex = secondJobId + "_dest"; + + long randomizeSeed = ((Regression) firstJob.getAnalysis()).getRandomizeSeed(); + DataFrameAnalyticsConfig secondJob = buildAnalytics(secondJobId, sourceIndex, secondJobDestIndex, null, + new Regression(DEPENDENT_VARIABLE_FIELD, boostedTreeParams, null, 50.0, randomizeSeed)); + + registerAnalytics(secondJob); + putAnalytics(secondJob); + + // Let's run both jobs in parallel and wait until they are finished + startAnalytics(firstJobId); + startAnalytics(secondJobId); + waitUntilAnalyticsIsStopped(firstJobId); + waitUntilAnalyticsIsStopped(secondJobId); + + // Now we compare they both used the same training rows + Set firstRunTrainingRowsIds = getTrainingRowsIds(firstJobDestIndex); + Set secondRunTrainingRowsIds = getTrainingRowsIds(secondJobDestIndex); + + assertThat(secondRunTrainingRowsIds, equalTo(firstRunTrainingRowsIds)); + } + private void initialize(String jobId) { this.jobId = jobId; this.sourceIndex = jobId + "_source_index"; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDataFrameAnalyticsAction.java index 2884cd331779e..1cbed7ed76613 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDataFrameAnalyticsAction.java @@ -111,7 +111,7 @@ protected ClusterBlockException checkBlock(PutDataFrameAnalyticsAction.Request r protected void masterOperation(Task task, PutDataFrameAnalyticsAction.Request request, ClusterState state, ActionListener listener) { validateConfig(request.getConfig()); - DataFrameAnalyticsConfig memoryCappedConfig = + DataFrameAnalyticsConfig preparedForPutConfig = new DataFrameAnalyticsConfig.Builder(request.getConfig(), maxModelMemoryLimit) .setCreateTime(Instant.now()) .setVersion(Version.CURRENT) @@ -120,11 +120,11 @@ protected void masterOperation(Task task, PutDataFrameAnalyticsAction.Request re if (licenseState.isAuthAllowed()) { final String username = securityContext.getUser().principal(); RoleDescriptor.IndicesPrivileges sourceIndexPrivileges = RoleDescriptor.IndicesPrivileges.builder() - .indices(memoryCappedConfig.getSource().getIndex()) + .indices(preparedForPutConfig.getSource().getIndex()) .privileges("read") .build(); RoleDescriptor.IndicesPrivileges destIndexPrivileges = RoleDescriptor.IndicesPrivileges.builder() - .indices(memoryCappedConfig.getDest().getIndex()) + .indices(preparedForPutConfig.getDest().getIndex()) .privileges("read", "index", "create_index") .build(); @@ -135,16 +135,16 @@ protected void masterOperation(Task task, PutDataFrameAnalyticsAction.Request re privRequest.indexPrivileges(sourceIndexPrivileges, destIndexPrivileges); ActionListener privResponseListener = ActionListener.wrap( - r -> handlePrivsResponse(username, memoryCappedConfig, r, listener), + r -> handlePrivsResponse(username, preparedForPutConfig, r, listener), listener::onFailure); client.execute(HasPrivilegesAction.INSTANCE, privRequest, privResponseListener); } else { updateDocMappingAndPutConfig( - memoryCappedConfig, + preparedForPutConfig, threadPool.getThreadContext().getHeaders(), ActionListener.wrap( - indexResponse -> listener.onResponse(new PutDataFrameAnalyticsAction.Response(memoryCappedConfig)), + indexResponse -> listener.onResponse(new PutDataFrameAnalyticsAction.Response(preparedForPutConfig)), listener::onFailure )); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/CustomProcessorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/CustomProcessorFactory.java index fd52a3fd8da58..77f0b127a2638 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/CustomProcessorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/CustomProcessorFactory.java @@ -24,12 +24,12 @@ public CustomProcessor create(DataFrameAnalysis analysis) { if (analysis instanceof Regression) { Regression regression = (Regression) analysis; return new DatasetSplittingCustomProcessor( - fieldNames, regression.getDependentVariable(), regression.getTrainingPercent()); + fieldNames, regression.getDependentVariable(), regression.getTrainingPercent(), regression.getRandomizeSeed()); } if (analysis instanceof Classification) { Classification classification = (Classification) analysis; return new DatasetSplittingCustomProcessor( - fieldNames, classification.getDependentVariable(), classification.getTrainingPercent()); + fieldNames, classification.getDependentVariable(), classification.getTrainingPercent(), classification.getRandomizeSeed()); } return row -> {}; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/DatasetSplittingCustomProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/DatasetSplittingCustomProcessor.java index ed42cf5198854..bf6284aa7a5c8 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/DatasetSplittingCustomProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/DatasetSplittingCustomProcessor.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.ml.dataframe.process.customprocessing; -import org.elasticsearch.common.Randomness; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.util.List; @@ -23,12 +22,13 @@ class DatasetSplittingCustomProcessor implements CustomProcessor { private final int dependentVariableIndex; private final double trainingPercent; - private final Random random = Randomness.get(); + private final Random random; private boolean isFirstRow = true; - DatasetSplittingCustomProcessor(List fieldNames, String dependentVariable, double trainingPercent) { + DatasetSplittingCustomProcessor(List fieldNames, String dependentVariable, double trainingPercent, long randomizeSeed) { this.dependentVariableIndex = findDependentVariableIndex(fieldNames, dependentVariable); this.trainingPercent = trainingPercent; + this.random = new Random(randomizeSeed); } private static int findDependentVariableIndex(List fieldNames, String dependentVariable) { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/DatasetSplittingCustomProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/DatasetSplittingCustomProcessorTests.java index d5973f8782461..d18adc3dcdb48 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/DatasetSplittingCustomProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/customprocessing/DatasetSplittingCustomProcessorTests.java @@ -24,6 +24,7 @@ public class DatasetSplittingCustomProcessorTests extends ESTestCase { private List fields; private int dependentVariableIndex; private String dependentVariable; + private long randomizeSeed; @Before public void setUpTests() { @@ -34,10 +35,11 @@ public void setUpTests() { } dependentVariableIndex = randomIntBetween(0, fieldCount - 1); dependentVariable = fields.get(dependentVariableIndex); + randomizeSeed = randomLong(); } public void testProcess_GivenRowsWithoutDependentVariableValue() { - CustomProcessor customProcessor = new DatasetSplittingCustomProcessor(fields, dependentVariable, 50.0); + CustomProcessor customProcessor = new DatasetSplittingCustomProcessor(fields, dependentVariable, 50.0, randomizeSeed); for (int i = 0; i < 100; i++) { String[] row = new String[fields.size()]; @@ -55,7 +57,7 @@ public void testProcess_GivenRowsWithoutDependentVariableValue() { } public void testProcess_GivenRowsWithDependentVariableValue_AndTrainingPercentIsHundred() { - CustomProcessor customProcessor = new DatasetSplittingCustomProcessor(fields, dependentVariable, 100.0); + CustomProcessor customProcessor = new DatasetSplittingCustomProcessor(fields, dependentVariable, 100.0, randomizeSeed); for (int i = 0; i < 100; i++) { String[] row = new String[fields.size()]; @@ -75,7 +77,7 @@ public void testProcess_GivenRowsWithDependentVariableValue_AndTrainingPercentIs public void testProcess_GivenRowsWithDependentVariableValue_AndTrainingPercentIsRandom() { double trainingPercent = randomDoubleBetween(1.0, 100.0, true); double trainingFraction = trainingPercent / 100; - CustomProcessor customProcessor = new DatasetSplittingCustomProcessor(fields, dependentVariable, trainingPercent); + CustomProcessor customProcessor = new DatasetSplittingCustomProcessor(fields, dependentVariable, trainingPercent, randomizeSeed); int runCount = 20; int rowsCount = 1000; @@ -121,7 +123,7 @@ public void testProcess_GivenRowsWithDependentVariableValue_AndTrainingPercentIs } public void testProcess_ShouldHaveAtLeastOneTrainingRow() { - CustomProcessor customProcessor = new DatasetSplittingCustomProcessor(fields, dependentVariable, 1.0); + CustomProcessor customProcessor = new DatasetSplittingCustomProcessor(fields, dependentVariable, 1.0, randomizeSeed); // We have some non-training rows and then a training row to check // we maintain the first training row and not just the first row diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/data_frame_analytics_crud.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/data_frame_analytics_crud.yml index a1d78b7444057..4335a50382a94 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/data_frame_analytics_crud.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/data_frame_analytics_crud.yml @@ -1456,7 +1456,8 @@ setup: "eta": 0.5, "maximum_number_trees": 400, "feature_bag_fraction": 0.3, - "training_percent": 60.3 + "training_percent": 60.3, + "randomize_seed": 42 } } } @@ -1472,7 +1473,8 @@ setup: "maximum_number_trees": 400, "feature_bag_fraction": 0.3, "prediction_field_name": "foo_prediction", - "training_percent": 60.3 + "training_percent": 60.3, + "randomize_seed": 42 } }} - is_true: create_time @@ -1796,7 +1798,8 @@ setup: "eta": 0.5, "maximum_number_trees": 400, "feature_bag_fraction": 0.3, - "training_percent": 60.3 + "training_percent": 60.3, + "randomize_seed": 24 } } } @@ -1813,6 +1816,7 @@ setup: "feature_bag_fraction": 0.3, "prediction_field_name": "foo_prediction", "training_percent": 60.3, + "randomize_seed": 24, "num_top_classes": 2 } }} @@ -1836,7 +1840,8 @@ setup: }, "analysis": { "regression": { - "dependent_variable": "foo" + "dependent_variable": "foo", + "randomize_seed": 42 } } } @@ -1848,7 +1853,8 @@ setup: "regression":{ "dependent_variable": "foo", "prediction_field_name": "foo_prediction", - "training_percent": 100.0 + "training_percent": 100.0, + "randomize_seed": 42 } }} - is_true: create_time From b48df75bfc1dee09b7ba642ccd35401da6391c96 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 29 Nov 2019 14:38:06 +0200 Subject: [PATCH 136/686] [ML] Mute data frame analytics BWC tests Until #49990 is backported to 7.x --- .../test/mixed_cluster/90_ml_data_frame_analytics_crud.yml | 5 +++++ .../test/old_cluster/90_ml_data_frame_analytics_crud.yml | 3 +++ .../upgraded_cluster/90_ml_data_frame_analytics_crud.yml | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml index b0cb91c4c0f5c..8082147160718 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,3 +1,8 @@ +setup: + - skip: + version: "all" + reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" + --- "Get old outlier_detection job": diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml index fe160bba15f23..ba2cf40411672 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,4 +1,7 @@ setup: + - skip: + version: "all" + reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" - do: index: diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml index 28ec80c6373a2..462a1fd76c011 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,3 +1,8 @@ +setup: + - skip: + version: "all" + reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" + --- "Get old cluster outlier_detection job": From f1234f25ab13d8b2aac48da3b123a2fcbcaeb939 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Tue, 10 Dec 2019 09:45:27 +0100 Subject: [PATCH 137/686] Make elasticsearch-node tools custom metadata-aware (#48390) The elasticsearch-node tools allow manipulating the on-disk cluster state. The tool is currently unaware of plugins and will therefore drop custom metadata from the cluster state once the state is written out again (as it skips over the custom metadata that it can't read). This commit preserves unknown customs when editing on-disk metadata through the elasticsearch-node command-line tools. --- .../testclusters/ElasticsearchNode.java | 12 +-- .../common/xcontent/XContentBuilder.java | 19 +++-- .../cli/EnvironmentAwareCommand.java | 13 ++- .../ElasticsearchNodeCommand.java | 9 +-- .../UnsafeBootstrapMasterCommand.java | 3 +- .../cluster/metadata/IndexMetaData.java | 3 - .../cluster/metadata/MetaData.java | 81 +++++++++++++++---- .../env/NodeRepurposeCommand.java | 5 +- .../env/OverrideNodeVersionCommand.java | 3 +- .../cluster/metadata/MetaDataTests.java | 2 +- .../metadata/ToAndFromJsonMetaDataTests.java | 2 +- .../gateway/MetaDataStateFormatTests.java | 51 ++++++++++-- .../LicensesMetaDataSerializationTests.java | 2 +- .../WatcherMetaDataSerializationTests.java | 2 +- 14 files changed, 153 insertions(+), 54 deletions(-) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java index de8e02ed0c7de..2f258733e7575 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java @@ -424,7 +424,7 @@ public synchronized void start() { if (plugins.isEmpty() == false) { logToProcessStdout("Installing " + plugins.size() + " plugins"); - plugins.forEach(plugin -> runElaticsearchBinScript( + plugins.forEach(plugin -> runElasticsearchBinScript( "elasticsearch-plugin", "install", "--batch", plugin.toString()) ); @@ -432,7 +432,7 @@ public synchronized void start() { if (getVersion().before("6.3.0") && testDistribution == TestDistribution.DEFAULT) { LOGGER.info("emulating the {} flavor for {} by installing x-pack", testDistribution, getVersion()); - runElaticsearchBinScript( + runElasticsearchBinScript( "elasticsearch-plugin", "install", "--batch", "x-pack" ); @@ -440,7 +440,7 @@ public synchronized void start() { if (keystoreSettings.isEmpty() == false || keystoreFiles.isEmpty() == false) { logToProcessStdout("Adding " + keystoreSettings.size() + " keystore settings and " + keystoreFiles.size() + " keystore files"); - runElaticsearchBinScript("elasticsearch-keystore", "create"); + runElasticsearchBinScript("elasticsearch-keystore", "create"); keystoreSettings.forEach((key, value) -> runElasticsearchBinScriptWithInput(value.toString(), "elasticsearch-keystore", "add", "-x", key) @@ -452,7 +452,7 @@ public synchronized void start() { if (file.exists() == false) { throw new TestClustersException("supplied keystore file " + file + " does not exist, require for " + this); } - runElaticsearchBinScript("elasticsearch-keystore", "add-file", entry.getKey(), file.getAbsolutePath()); + runElasticsearchBinScript("elasticsearch-keystore", "add-file", entry.getKey(), file.getAbsolutePath()); } } @@ -467,7 +467,7 @@ public synchronized void start() { if (credentials.isEmpty() == false) { logToProcessStdout("Setting up " + credentials.size() + " users"); - credentials.forEach(paramMap -> runElaticsearchBinScript( + credentials.forEach(paramMap -> runElasticsearchBinScript( getVersion().onOrAfter("6.3.0") ? "elasticsearch-users" : "x-pack/users", paramMap.entrySet().stream() .flatMap(entry -> Stream.of(entry.getKey(), entry.getValue())) @@ -663,7 +663,7 @@ private void runElasticsearchBinScriptWithInput(String input, String tool, Strin } } - private void runElaticsearchBinScript(String tool, String... args) { + private void runElasticsearchBinScript(String tool, String... args) { runElasticsearchBinScriptWithInput("", tool, args); } diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentBuilder.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentBuilder.java index 51a4f86a0d3b2..20fde0891b6f8 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentBuilder.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentBuilder.java @@ -819,7 +819,7 @@ private void unknownValue(Object value, boolean ensureNoSelfReferences) throws I } else if (value instanceof Map) { @SuppressWarnings("unchecked") final Map valueMap = (Map) value; - map(valueMap, ensureNoSelfReferences); + map(valueMap, ensureNoSelfReferences, true); } else if (value instanceof Iterable) { value((Iterable) value, ensureNoSelfReferences); } else if (value instanceof Object[]) { @@ -867,10 +867,15 @@ public XContentBuilder field(String name, Map values) throws IOE } public XContentBuilder map(Map values) throws IOException { - return map(values, true); + return map(values, true, true); } - private XContentBuilder map(Map values, boolean ensureNoSelfReferences) throws IOException { + /** writes a map without the start object and end object headers */ + public XContentBuilder mapContents(Map values) throws IOException { + return map(values, true, false); + } + + private XContentBuilder map(Map values, boolean ensureNoSelfReferences, boolean writeStartAndEndHeaders) throws IOException { if (values == null) { return nullValue(); } @@ -881,13 +886,17 @@ private XContentBuilder map(Map values, boolean ensureNoSelfReference ensureNoSelfReferences(values); } - startObject(); + if (writeStartAndEndHeaders) { + startObject(); + } for (Map.Entry value : values.entrySet()) { field(value.getKey()); // pass ensureNoSelfReferences=false as we already performed the check at a higher level unknownValue(value.getValue(), false); } - endObject(); + if (writeStartAndEndHeaders) { + endObject(); + } return this; } diff --git a/server/src/main/java/org/elasticsearch/cli/EnvironmentAwareCommand.java b/server/src/main/java/org/elasticsearch/cli/EnvironmentAwareCommand.java index 6fc3349c76233..1d3a31f0a72d9 100644 --- a/server/src/main/java/org/elasticsearch/cli/EnvironmentAwareCommand.java +++ b/server/src/main/java/org/elasticsearch/cli/EnvironmentAwareCommand.java @@ -88,14 +88,19 @@ protected void execute(Terminal terminal, OptionSet options) throws Exception { /** Create an {@link Environment} for the command to use. Overrideable for tests. */ protected Environment createEnv(final Map settings) throws UserException { + return createEnv(Settings.EMPTY, settings); + } + + /** Create an {@link Environment} for the command to use. Overrideable for tests. */ + protected final Environment createEnv(final Settings baseSettings, final Map settings) throws UserException { final String esPathConf = System.getProperty("es.path.conf"); if (esPathConf == null) { throw new UserException(ExitCodes.CONFIG, "the system property [es.path.conf] must be set"); } - return InternalSettingsPreparer.prepareEnvironment(Settings.EMPTY, settings, - getConfigPath(esPathConf), - // HOSTNAME is set by elasticsearch-env and elasticsearch-env.bat so it is always available - () -> System.getenv("HOSTNAME")); + return InternalSettingsPreparer.prepareEnvironment(baseSettings, settings, + getConfigPath(esPathConf), + // HOSTNAME is set by elasticsearch-env and elasticsearch-env.bat so it is always available + () -> System.getenv("HOSTNAME")); } @SuppressForbidden(reason = "need path to construct environment") diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/ElasticsearchNodeCommand.java b/server/src/main/java/org/elasticsearch/cluster/coordination/ElasticsearchNodeCommand.java index a65934c767769..800269520e366 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/ElasticsearchNodeCommand.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/ElasticsearchNodeCommand.java @@ -26,7 +26,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.cli.EnvironmentAwareCommand; import org.elasticsearch.cli.Terminal; -import org.elasticsearch.cluster.ClusterModule; import org.elasticsearch.cluster.metadata.Manifest; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.collect.Tuple; @@ -42,7 +41,6 @@ public abstract class ElasticsearchNodeCommand extends EnvironmentAwareCommand { private static final Logger logger = LogManager.getLogger(ElasticsearchNodeCommand.class); - protected final NamedXContentRegistry namedXContentRegistry; protected static final String DELIMITER = "------------------------------------------------------------------------\n"; static final String STOP_WARNING_MSG = @@ -61,7 +59,6 @@ public abstract class ElasticsearchNodeCommand extends EnvironmentAwareCommand { public ElasticsearchNodeCommand(String description) { super(description); - namedXContentRegistry = new NamedXContentRegistry(ClusterModule.getNamedXWriteables()); } protected void processNodePaths(Terminal terminal, OptionSet options, Environment env) throws IOException { @@ -80,7 +77,7 @@ protected void processNodePaths(Terminal terminal, OptionSet options, Environmen protected Tuple loadMetaData(Terminal terminal, Path[] dataPaths) throws IOException { terminal.println(Terminal.Verbosity.VERBOSE, "Loading manifest file"); - final Manifest manifest = Manifest.FORMAT.loadLatestState(logger, namedXContentRegistry, dataPaths); + final Manifest manifest = Manifest.FORMAT.loadLatestState(logger, NamedXContentRegistry.EMPTY, dataPaths); if (manifest == null) { throw new ElasticsearchException(NO_MANIFEST_FILE_FOUND_MSG); @@ -89,8 +86,8 @@ protected Tuple loadMetaData(Terminal terminal, Path[] dataP throw new ElasticsearchException(GLOBAL_GENERATION_MISSING_MSG); } terminal.println(Terminal.Verbosity.VERBOSE, "Loading global metadata file"); - final MetaData metaData = MetaData.FORMAT.loadGeneration(logger, namedXContentRegistry, manifest.getGlobalGeneration(), - dataPaths); + final MetaData metaData = MetaData.FORMAT_PRESERVE_CUSTOMS.loadGeneration( + logger, NamedXContentRegistry.EMPTY, manifest.getGlobalGeneration(), dataPaths); if (metaData == null) { throw new ElasticsearchException(NO_GLOBAL_METADATA_MSG + " [generation = " + manifest.getGlobalGeneration() + "]"); } diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/UnsafeBootstrapMasterCommand.java b/server/src/main/java/org/elasticsearch/cluster/coordination/UnsafeBootstrapMasterCommand.java index c15e832142eaf..05bc0116c13c6 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/UnsafeBootstrapMasterCommand.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/UnsafeBootstrapMasterCommand.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeMetaData; import org.elasticsearch.node.Node; @@ -84,7 +85,7 @@ protected boolean validateBeforeLock(Terminal terminal, Environment env) { protected void processNodePaths(Terminal terminal, Path[] dataPaths, Environment env) throws IOException { terminal.println(Terminal.Verbosity.VERBOSE, "Loading node metadata"); - final NodeMetaData nodeMetaData = NodeMetaData.FORMAT.loadLatestState(logger, namedXContentRegistry, dataPaths); + final NodeMetaData nodeMetaData = NodeMetaData.FORMAT.loadLatestState(logger, NamedXContentRegistry.EMPTY, dataPaths); if (nodeMetaData == null) { throw new ElasticsearchException(NO_NODE_METADATA_FOUND_MSG); } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java index d8fd88696e9f6..f8e1b48c6dd83 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java @@ -45,7 +45,6 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -1421,8 +1420,6 @@ public void toXContent(XContentBuilder builder, IndexMetaData state) throws IOEx @Override public IndexMetaData fromXContent(XContentParser parser) throws IOException { - assert parser.getXContentRegistry() != NamedXContentRegistry.EMPTY - : "loading index metadata requires a working named xcontent registry"; return Builder.fromXContent(parser); } }; diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java index ce67e5b72f1d1..482d57bed54b5 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java @@ -755,7 +755,7 @@ public static Diff readDiffFrom(StreamInput in) throws IOException { } public static MetaData fromXContent(XContentParser parser) throws IOException { - return Builder.fromXContent(parser); + return Builder.fromXContent(parser, false); } @Override @@ -1277,7 +1277,7 @@ public static void toXContent(MetaData metaData, XContentBuilder builder, ToXCon builder.endObject(); } - public static MetaData fromXContent(XContentParser parser) throws IOException { + public static MetaData fromXContent(XContentParser parser, boolean preserveUnknownCustoms) throws IOException { Builder builder = new Builder(); // we might get here after the meta-data element, or on a fresh parser @@ -1327,8 +1327,13 @@ public static MetaData fromXContent(XContentParser parser) throws IOException { Custom custom = parser.namedObject(Custom.class, currentFieldName, null); builder.putCustom(custom.getWriteableName(), custom); } catch (NamedObjectNotFoundException ex) { - logger.warn("Skipping unknown custom object with type {}", currentFieldName); - parser.skipChildren(); + if (preserveUnknownCustoms) { + logger.warn("Adding unknown custom object with type {}", currentFieldName); + builder.putCustom(currentFieldName, new UnknownGatewayOnlyCustom(parser.mapOrdered())); + } else { + logger.warn("Skipping unknown custom object with type {}", currentFieldName); + parser.skipChildren(); + } } } } else if (token.isValue()) { @@ -1349,6 +1354,45 @@ public static MetaData fromXContent(XContentParser parser) throws IOException { } } + public static class UnknownGatewayOnlyCustom implements Custom { + + private final Map contents; + + UnknownGatewayOnlyCustom(Map contents) { + this.contents = contents; + } + + @Override + public EnumSet context() { + return EnumSet.of(MetaData.XContentContext.API, MetaData.XContentContext.GATEWAY); + } + + @Override + public Diff diff(Custom previousState) { + throw new UnsupportedOperationException(); + } + + @Override + public String getWriteableName() { + throw new UnsupportedOperationException(); + } + + @Override + public Version getMinimalSupportedVersion() { + throw new UnsupportedOperationException(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.mapContents(contents); + } + } + private static final ToXContent.Params FORMAT_PARAMS; static { Map params = new HashMap<>(2); @@ -1360,16 +1404,25 @@ public static MetaData fromXContent(XContentParser parser) throws IOException { /** * State format for {@link MetaData} to write to and load from disk */ - public static final MetaDataStateFormat FORMAT = new MetaDataStateFormat(GLOBAL_STATE_FILE_PREFIX) { + public static final MetaDataStateFormat FORMAT = createMetaDataStateFormat(false); - @Override - public void toXContent(XContentBuilder builder, MetaData state) throws IOException { - Builder.toXContent(state, builder, FORMAT_PARAMS); - } + /** + * Special state format for {@link MetaData} to write to and load from disk, preserving unknown customs + */ + public static final MetaDataStateFormat FORMAT_PRESERVE_CUSTOMS = createMetaDataStateFormat(true); - @Override - public MetaData fromXContent(XContentParser parser) throws IOException { - return Builder.fromXContent(parser); - } - }; + private static MetaDataStateFormat createMetaDataStateFormat(boolean preserveUnknownCustoms) { + return new MetaDataStateFormat(GLOBAL_STATE_FILE_PREFIX) { + + @Override + public void toXContent(XContentBuilder builder, MetaData state) throws IOException { + Builder.toXContent(state, builder, FORMAT_PARAMS); + } + + @Override + public MetaData fromXContent(XContentParser parser) throws IOException { + return Builder.fromXContent(parser, preserveUnknownCustoms); + } + }; + } } diff --git a/server/src/main/java/org/elasticsearch/env/NodeRepurposeCommand.java b/server/src/main/java/org/elasticsearch/env/NodeRepurposeCommand.java index 20b5552dfa8f8..25b4f79866eaa 100644 --- a/server/src/main/java/org/elasticsearch/env/NodeRepurposeCommand.java +++ b/server/src/main/java/org/elasticsearch/env/NodeRepurposeCommand.java @@ -29,6 +29,7 @@ import org.elasticsearch.cluster.metadata.Manifest; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.gateway.WriteStateException; @@ -165,7 +166,7 @@ private String toIndexName(NodeEnvironment.NodePath[] nodePaths, String uuid) { indexPaths[i] = nodePaths[i].resolve(uuid); } try { - IndexMetaData metaData = IndexMetaData.FORMAT.loadLatestState(logger, namedXContentRegistry, indexPaths); + IndexMetaData metaData = IndexMetaData.FORMAT.loadLatestState(logger, NamedXContentRegistry.EMPTY, indexPaths); return metaData.getIndex().getName(); } catch (Exception e) { return "no name for uuid: " + uuid + ": " + e; @@ -194,7 +195,7 @@ private void rewriteManifest(Terminal terminal, Manifest manifest, Path[] dataPa private Manifest loadManifest(Terminal terminal, Path[] dataPaths) throws IOException { terminal.println(Terminal.Verbosity.VERBOSE, "Loading manifest"); - final Manifest manifest = Manifest.FORMAT.loadLatestState(logger, namedXContentRegistry, dataPaths); + final Manifest manifest = Manifest.FORMAT.loadLatestState(logger, NamedXContentRegistry.EMPTY, dataPaths); if (manifest == null) { terminal.println(Terminal.Verbosity.SILENT, PRE_V7_MESSAGE); diff --git a/server/src/main/java/org/elasticsearch/env/OverrideNodeVersionCommand.java b/server/src/main/java/org/elasticsearch/env/OverrideNodeVersionCommand.java index 34c7e9599e07f..f50bdf081ef85 100644 --- a/server/src/main/java/org/elasticsearch/env/OverrideNodeVersionCommand.java +++ b/server/src/main/java/org/elasticsearch/env/OverrideNodeVersionCommand.java @@ -25,6 +25,7 @@ import org.elasticsearch.Version; import org.elasticsearch.cli.Terminal; import org.elasticsearch.cluster.coordination.ElasticsearchNodeCommand; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import java.io.IOException; import java.nio.file.Path; @@ -74,7 +75,7 @@ public OverrideNodeVersionCommand() { protected void processNodePaths(Terminal terminal, Path[] dataPaths, Environment env) throws IOException { final Path[] nodePaths = Arrays.stream(toNodePaths(dataPaths)).map(p -> p.path).toArray(Path[]::new); final NodeMetaData nodeMetaData - = new NodeMetaData.NodeMetaDataStateFormat(true).loadLatestState(logger, namedXContentRegistry, nodePaths); + = new NodeMetaData.NodeMetaDataStateFormat(true).loadLatestState(logger, NamedXContentRegistry.EMPTY, nodePaths); if (nodeMetaData == null) { throw new ElasticsearchException(NO_METADATA_MESSAGE); } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataTests.java index 36a78119c7665..7d2b10beb3279 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataTests.java @@ -365,7 +365,7 @@ public void testUnknownFieldClusterMetaData() throws IOException { .endObject() .endObject()); try (XContentParser parser = createParser(JsonXContent.jsonXContent, metadata)) { - MetaData.Builder.fromXContent(parser); + MetaData.Builder.fromXContent(parser, randomBoolean()); fail(); } catch (IllegalArgumentException e) { assertEquals("Unexpected field [random]", e.getMessage()); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetaDataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetaDataTests.java index e2d0fcf5188a6..0338a64b6fe7e 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetaDataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetaDataTests.java @@ -140,7 +140,7 @@ public void testSimpleJsonFromAndTo() throws IOException { String metaDataSource = MetaData.Builder.toXContent(metaData); - MetaData parsedMetaData = MetaData.Builder.fromXContent(createParser(JsonXContent.jsonXContent, metaDataSource)); + MetaData parsedMetaData = MetaData.Builder.fromXContent(createParser(JsonXContent.jsonXContent, metaDataSource), false); IndexMetaData indexMetaData = parsedMetaData.index("test1"); assertThat(indexMetaData.primaryTerm(0), equalTo(1L)); diff --git a/server/src/test/java/org/elasticsearch/gateway/MetaDataStateFormatTests.java b/server/src/test/java/org/elasticsearch/gateway/MetaDataStateFormatTests.java index 40f3bd8a01623..c7dab0dc4d4a0 100644 --- a/server/src/test/java/org/elasticsearch/gateway/MetaDataStateFormatTests.java +++ b/server/src/test/java/org/elasticsearch/gateway/MetaDataStateFormatTests.java @@ -61,6 +61,8 @@ import java.util.stream.StreamSupport; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; @@ -80,7 +82,7 @@ public void toXContent(XContentBuilder builder, MetaData state) { @Override public MetaData fromXContent(XContentParser parser) throws IOException { - return MetaData.Builder.fromXContent(parser); + return MetaData.Builder.fromXContent(parser, false); } }; Path tmp = createTempDir(); @@ -233,7 +235,23 @@ public static void corruptFile(Path fileToCorrupt, Logger logger) throws IOExcep } } - public void testLoadState() throws IOException { + public void testLoadStateWithoutMissingCustoms() throws IOException { + runLoadStateTest(false, false); + } + + public void testLoadStateWithoutMissingCustomsButPreserved() throws IOException { + runLoadStateTest(false, true); + } + + public void testLoadStateWithMissingCustomsButPreserved() throws IOException { + runLoadStateTest(true, true); + } + + public void testLoadStateWithMissingCustomsAndNotPreserved() throws IOException { + runLoadStateTest(true, false); + } + + private void runLoadStateTest(boolean hasMissingCustoms, boolean preserveUnknownCustoms) throws IOException { final Path[] dirs = new Path[randomIntBetween(1, 5)]; int numStates = randomIntBetween(1, 5); List meta = new ArrayList<>(); @@ -241,7 +259,7 @@ public void testLoadState() throws IOException { meta.add(randomMeta()); } Set corruptedFiles = new HashSet<>(); - MetaDataStateFormat format = metaDataFormat(); + MetaDataStateFormat format = metaDataFormat(preserveUnknownCustoms); for (int i = 0; i < dirs.length; i++) { dirs[i] = createTempDir(); Files.createDirectories(dirs[i].resolve(MetaDataStateFormat.STATE_DIR_NAME)); @@ -258,11 +276,12 @@ public void testLoadState() throws IOException { } List dirList = Arrays.asList(dirs); Collections.shuffle(dirList, random()); - MetaData loadedMetaData = format.loadLatestState(logger, xContentRegistry(), dirList.toArray(new Path[0])); + MetaData loadedMetaData = format.loadLatestState(logger, hasMissingCustoms ? + NamedXContentRegistry.EMPTY : xContentRegistry(), dirList.toArray(new Path[0])); MetaData latestMetaData = meta.get(numStates-1); assertThat(loadedMetaData.clusterUUID(), not(equalTo("_na_"))); assertThat(loadedMetaData.clusterUUID(), equalTo(latestMetaData.clusterUUID())); - ImmutableOpenMap indices = loadedMetaData.indices(); + ImmutableOpenMap indices = loadedMetaData.indices(); assertThat(indices.size(), equalTo(latestMetaData.indices().size())); for (IndexMetaData original : latestMetaData) { IndexMetaData deserialized = indices.get(original.getIndex().getName()); @@ -275,7 +294,23 @@ public void testLoadState() throws IOException { } // make sure the index tombstones are the same too - assertThat(loadedMetaData.indexGraveyard(), equalTo(latestMetaData.indexGraveyard())); + if (hasMissingCustoms) { + if (preserveUnknownCustoms) { + assertNotNull(loadedMetaData.custom(IndexGraveyard.TYPE)); + assertThat(loadedMetaData.custom(IndexGraveyard.TYPE), instanceOf(MetaData.UnknownGatewayOnlyCustom.class)); + + // check that we reserialize unknown metadata correctly again + final Path tempdir = createTempDir(); + metaDataFormat(randomBoolean()).write(loadedMetaData, tempdir); + final MetaData reloadedMetaData = metaDataFormat(randomBoolean()).loadLatestState(logger, xContentRegistry(), tempdir); + assertThat(reloadedMetaData.indexGraveyard(), equalTo(latestMetaData.indexGraveyard())); + } else { + assertNotNull(loadedMetaData.indexGraveyard()); + assertThat(loadedMetaData.indexGraveyard().getTombstones(), hasSize(0)); + } + } else { + assertThat(loadedMetaData.indexGraveyard(), equalTo(latestMetaData.indexGraveyard())); + } // now corrupt all the latest ones and make sure we fail to load the state for (int i = 0; i < dirs.length; i++) { @@ -419,7 +454,7 @@ public void testFailRandomlyAndReadAnyState() throws IOException { writeAndReadStateSuccessfully(format, paths); } - private static MetaDataStateFormat metaDataFormat() { + private static MetaDataStateFormat metaDataFormat(boolean preserveUnknownCustoms) { return new MetaDataStateFormat(MetaData.GLOBAL_STATE_FILE_PREFIX) { @Override public void toXContent(XContentBuilder builder, MetaData state) throws IOException { @@ -428,7 +463,7 @@ public void toXContent(XContentBuilder builder, MetaData state) throws IOExcepti @Override public MetaData fromXContent(XContentParser parser) throws IOException { - return MetaData.Builder.fromXContent(parser); + return MetaData.Builder.fromXContent(parser, preserveUnknownCustoms); } }; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetaDataSerializationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetaDataSerializationTests.java index d7799959f6cce..084d965a6e74b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetaDataSerializationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetaDataSerializationTests.java @@ -80,7 +80,7 @@ public void testLicenseMetadataParsingDoesNotSwallowOtherMetaData() throws Excep builder = metaDataBuilder.build().toXContent(builder, params); builder.endObject(); // deserialize metadata again - MetaData metaData = MetaData.Builder.fromXContent(createParser(builder)); + MetaData metaData = MetaData.Builder.fromXContent(createParser(builder), randomBoolean()); // check that custom metadata still present assertThat(metaData.custom(licensesMetaData.getWriteableName()), notNullValue()); assertThat(metaData.custom(repositoriesMetaData.getWriteableName()), notNullValue()); diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetaDataSerializationTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetaDataSerializationTests.java index 0556b8535e428..75e5bc1073e69 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetaDataSerializationTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetaDataSerializationTests.java @@ -64,7 +64,7 @@ public void testWatcherMetadataParsingDoesNotSwallowOtherMetaData() throws Excep builder = metaDataBuilder.build().toXContent(builder, params); builder.endObject(); // deserialize metadata again - MetaData metaData = MetaData.Builder.fromXContent(createParser(builder)); + MetaData metaData = MetaData.Builder.fromXContent(createParser(builder), randomBoolean()); // check that custom metadata still present assertThat(metaData.custom(watcherMetaData.getWriteableName()), notNullValue()); assertThat(metaData.custom(repositoriesMetaData.getWriteableName()), notNullValue()); From a6a28a765847cc5889792c7c484e721401d0acc3 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 10 Dec 2019 11:04:30 +0100 Subject: [PATCH 138/686] Improve Snapshot Finalization Ex. Handling (#49995) * Improve Snapshot Finalization Ex. Handling Like in #49989 we can get into a situation where the setting of the repository generation (during snapshot finalization) in the cluster state fails due to master failing over. In this case we should not try to execute the next cluster state update that will remove the snapshot from the cluster state. Closes #49989 --- .../elasticsearch/snapshots/SnapshotsService.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index c38462e240747..a48f893dfd408 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -35,6 +35,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateApplier; import org.elasticsearch.cluster.ClusterStateUpdateTask; +import org.elasticsearch.cluster.NotMasterException; import org.elasticsearch.cluster.RepositoryCleanupInProgress; import org.elasticsearch.cluster.RestoreInProgress; import org.elasticsearch.cluster.SnapshotDeletionsInProgress; @@ -42,6 +43,7 @@ import org.elasticsearch.cluster.SnapshotsInProgress.ShardSnapshotStatus; import org.elasticsearch.cluster.SnapshotsInProgress.ShardState; import org.elasticsearch.cluster.SnapshotsInProgress.State; +import org.elasticsearch.cluster.coordination.FailedToCommitClusterStateException; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.MetaData; @@ -1051,8 +1053,16 @@ protected void doRun() { @Override public void onFailure(final Exception e) { Snapshot snapshot = entry.snapshot(); - logger.warn(() -> new ParameterizedMessage("[{}] failed to finalize snapshot", snapshot), e); - removeSnapshotFromClusterState(snapshot, null, e); + if (ExceptionsHelper.unwrap(e, NotMasterException.class, FailedToCommitClusterStateException.class) != null) { + // Failure due to not being master any more, don't try to remove snapshot from cluster state the next master + // will try ending this snapshot again + logger.debug(() -> new ParameterizedMessage( + "[{}] failed to update cluster state during snapshot finalization", snapshot), e); + endingSnapshots.remove(snapshot); + } else { + logger.warn(() -> new ParameterizedMessage("[{}] failed to finalize snapshot", snapshot), e); + removeSnapshotFromClusterState(snapshot, null, e); + } } }); } From e699f1aefab73a7a339e04a01b90a63a2ec9731d Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Tue, 10 Dec 2019 10:44:31 +0000 Subject: [PATCH 139/686] Fix query analyzer logic for mixed conjunctions of terms and ranges (#49803) When the query analyzer examines a conjunction containing both terms and ranges, it should only include ranges in the minimum_should_match calculation if there are no other range queries on that same field within the conjunction. This is because we cannot build a selection query over disjoint ranges on the same field, and it is not easy to check if two range queries have an overlap. The current logic to calculate this just sets minimum_should_match to 1 or 0, dependent on whether or not the current range is over a field that has already been seen. However, this can be incorrect in the case that there are terms in the same match group which adjust the minimum_should_match downwards. Instead, the logic should be changed to match the terms extraction, whereby we adjust minimum_should_match downwards if we have already seen a range field. Fixes #49684 --- .../percolator/QueryAnalyzer.java | 66 +++++---- .../percolator/QueryAnalyzerTests.java | 132 ++++++++++++++++++ 2 files changed, 168 insertions(+), 30 deletions(-) diff --git a/modules/percolator/src/main/java/org/elasticsearch/percolator/QueryAnalyzer.java b/modules/percolator/src/main/java/org/elasticsearch/percolator/QueryAnalyzer.java index 362c8870f652d..f08600cdfd0e9 100644 --- a/modules/percolator/src/main/java/org/elasticsearch/percolator/QueryAnalyzer.java +++ b/modules/percolator/src/main/java/org/elasticsearch/percolator/QueryAnalyzer.java @@ -232,7 +232,7 @@ private static Result handleConjunction(List conjunctionsWithUnknowns) { List conjunctions = conjunctionsWithUnknowns.stream().filter(r -> r.isUnknown() == false).collect(Collectors.toList()); if (conjunctions.isEmpty()) { if (conjunctionsWithUnknowns.isEmpty()) { - throw new IllegalArgumentException("Must have at least on conjunction sub result"); + throw new IllegalArgumentException("Must have at least one conjunction sub result"); } return conjunctionsWithUnknowns.get(0); // all conjunctions are unknown, so just return the first one } @@ -247,47 +247,53 @@ private static Result handleConjunction(List conjunctionsWithUnknowns) { int msm = 0; boolean verified = conjunctionsWithUnknowns.size() == conjunctions.size(); boolean matchAllDocs = true; - boolean hasDuplicateTerms = false; Set extractions = new HashSet<>(); Set seenRangeFields = new HashSet<>(); for (Result result : conjunctions) { - // In case that there are duplicate query extractions we need to be careful with - // incrementing msm, - // because that could lead to valid matches not becoming candidate matches: - // query: (field:val1 AND field:val2) AND (field:val2 AND field:val3) - // doc: field: val1 val2 val3 - // So lets be protective and decrease the msm: + int resultMsm = result.minimumShouldMatch; for (QueryExtraction queryExtraction : result.extractions) { if (queryExtraction.range != null) { // In case of range queries each extraction does not simply increment the - // minimum_should_match - // for that percolator query like for a term based extraction, so that can lead - // to more false - // positives for percolator queries with range queries than term based queries. - // The is because the way number fields are extracted from the document to be - // percolated. - // Per field a single range is extracted and if a percolator query has two or - // more range queries - // on the same field, then the minimum should match can be higher than clauses - // in the CoveringQuery. - // Therefore right now the minimum should match is incremented once per number - // field when processing - // the percolator query at index time. - if (seenRangeFields.add(queryExtraction.range.fieldName)) { - resultMsm = 1; - } else { - resultMsm = 0; + // minimum_should_match for that percolator query like for a term based extraction, + // so that can lead to more false positives for percolator queries with range queries + // than term based queries. + // This is because the way number fields are extracted from the document to be + // percolated. Per field a single range is extracted and if a percolator query has two or + // more range queries on the same field, then the minimum should match can be higher than clauses + // in the CoveringQuery. Therefore right now the minimum should match is only incremented once per + // number field when processing the percolator query at index time. + // For multiple ranges within a single extraction (ie from an existing conjunction or disjunction) + // then this will already have been taken care of, so we only check against fieldnames from + // previously processed extractions, and don't add to the seenRangeFields list until all + // extractions from this result are processed + if (seenRangeFields.contains(queryExtraction.range.fieldName)) { + resultMsm = Math.max(0, resultMsm - 1); + verified = false; } } - - if (extractions.contains(queryExtraction)) { - resultMsm = Math.max(0, resultMsm - 1); - verified = false; + else { + // In case that there are duplicate term query extractions we need to be careful with + // incrementing msm, because that could lead to valid matches not becoming candidate matches: + // query: (field:val1 AND field:val2) AND (field:val2 AND field:val3) + // doc: field: val1 val2 val3 + // So lets be protective and decrease the msm: + if (extractions.contains(queryExtraction)) { + resultMsm = Math.max(0, resultMsm - 1); + verified = false; + } } } msm += resultMsm; + // add range fields from this Result to the seenRangeFields set so that minimumShouldMatch is correctly + // calculated for subsequent Results + result.extractions.stream() + .map(e -> e.range) + .filter(Objects::nonNull) + .map(e -> e.fieldName) + .forEach(seenRangeFields::add); + if (result.verified == false // If some inner extractions are optional, the result can't be verified || result.minimumShouldMatch < result.extractions.size()) { @@ -299,7 +305,7 @@ private static Result handleConjunction(List conjunctionsWithUnknowns) { if (matchAllDocs) { return new Result(matchAllDocs, verified); } else { - return new Result(verified, extractions, hasDuplicateTerms ? 1 : msm); + return new Result(verified, extractions, msm); } } diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryAnalyzerTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryAnalyzerTests.java index 91c815c40322e..1c00d0555b41a 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryAnalyzerTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryAnalyzerTests.java @@ -78,6 +78,7 @@ import static org.elasticsearch.percolator.QueryAnalyzer.analyze; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; public class QueryAnalyzerTests extends ESTestCase { @@ -1208,4 +1209,135 @@ public void testIntervalQueries() { assertTermsEqual(result.extractions, new Term("field", "a")); } + public void testCombinedRangeAndTermWithMinimumShouldMatch() { + + Query disj = new BooleanQuery.Builder() + .add(IntPoint.newRangeQuery("i", 0, 10), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v1")), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v1")), Occur.SHOULD) + .setMinimumNumberShouldMatch(2) + .build(); + + Result r = analyze(disj, Version.CURRENT); + assertThat(r.minimumShouldMatch, equalTo(1)); + assertThat(r.extractions, hasSize(2)); + assertFalse(r.matchAllDocs); + assertFalse(r.verified); + + Query q = new BooleanQuery.Builder() + .add(IntPoint.newRangeQuery("i", 0, 10), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v1")), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v1")), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v1")), Occur.FILTER) + .setMinimumNumberShouldMatch(2) + .build(); + + Result result = analyze(q, Version.CURRENT); + assertThat(result.minimumShouldMatch, equalTo(1)); + assertThat(result.extractions.size(), equalTo(2)); + assertFalse(result.verified); + assertFalse(result.matchAllDocs); + + q = new BooleanQuery.Builder() + .add(q, Occur.MUST) + .add(q, Occur.MUST) + .build(); + + result = analyze(q, Version.CURRENT); + assertThat(result.minimumShouldMatch, equalTo(1)); + assertThat(result.extractions.size(), equalTo(2)); + assertFalse(result.verified); + assertFalse(result.matchAllDocs); + + Query q2 = new BooleanQuery.Builder() + .add(new TermQuery(new Term("f", "v1")), Occur.FILTER) + .add(IntPoint.newRangeQuery("i", 15, 20), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v2")), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v2")), Occur.MUST) + .setMinimumNumberShouldMatch(1) + .build(); + + result = analyze(q2, Version.CURRENT); + assertThat(result.minimumShouldMatch, equalTo(2)); + assertThat(result.extractions, hasSize(3)); + assertFalse(result.verified); + assertFalse(result.matchAllDocs); + + // multiple range queries on different fields + Query q3 = new BooleanQuery.Builder() + .add(IntPoint.newRangeQuery("i", 15, 20), Occur.SHOULD) + .add(IntPoint.newRangeQuery("i2", 15, 20), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v1")), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v2")), Occur.MUST) + .setMinimumNumberShouldMatch(1) + .build(); + result = analyze(q3, Version.CURRENT); + assertThat(result.minimumShouldMatch, equalTo(2)); + assertThat(result.extractions, hasSize(4)); + assertFalse(result.verified); + assertFalse(result.matchAllDocs); + + // multiple disjoint range queries on the same field + Query q4 = new BooleanQuery.Builder() + .add(IntPoint.newRangeQuery("i", 15, 20), Occur.SHOULD) + .add(IntPoint.newRangeQuery("i", 25, 30), Occur.SHOULD) + .add(IntPoint.newRangeQuery("i", 35, 40), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v1")), Occur.SHOULD) + .add(new TermQuery(new Term("f", "v2")), Occur.MUST) + .setMinimumNumberShouldMatch(1) + .build(); + result = analyze(q4, Version.CURRENT); + assertThat(result.minimumShouldMatch, equalTo(2)); + assertThat(result.extractions, hasSize(5)); + assertFalse(result.verified); + assertFalse(result.matchAllDocs); + + // multiple conjunction range queries on the same field + Query q5 = new BooleanQuery.Builder() + .add(new BooleanQuery.Builder() + .add(IntPoint.newRangeQuery("i", 15, 20), Occur.MUST) + .add(IntPoint.newRangeQuery("i", 25, 30), Occur.MUST) + .build(), Occur.MUST) + .add(IntPoint.newRangeQuery("i", 35, 40), Occur.MUST) + .add(new TermQuery(new Term("f", "v2")), Occur.MUST) + .build(); + result = analyze(q5, Version.CURRENT); + assertThat(result.minimumShouldMatch, equalTo(2)); + assertThat(result.extractions, hasSize(4)); + assertFalse(result.verified); + assertFalse(result.matchAllDocs); + + // multiple conjunction range queries on different fields + Query q6 = new BooleanQuery.Builder() + .add(new BooleanQuery.Builder() + .add(IntPoint.newRangeQuery("i", 15, 20), Occur.MUST) + .add(IntPoint.newRangeQuery("i2", 25, 30), Occur.MUST) + .build(), Occur.MUST) + .add(IntPoint.newRangeQuery("i", 35, 40), Occur.MUST) + .add(new TermQuery(new Term("f", "v2")), Occur.MUST) + .build(); + result = analyze(q6, Version.CURRENT); + assertThat(result.minimumShouldMatch, equalTo(3)); + assertThat(result.extractions, hasSize(4)); + assertFalse(result.verified); + assertFalse(result.matchAllDocs); + + // mixed term and range conjunctions + Query q7 = new BooleanQuery.Builder() + .add(new BooleanQuery.Builder() + .add(IntPoint.newRangeQuery("i", 1, 2), Occur.MUST) + .add(new TermQuery(new Term("f", "1")), Occur.MUST) + .build(), Occur.MUST) + .add(new BooleanQuery.Builder() + .add(IntPoint.newRangeQuery("i", 1, 2), Occur.MUST) + .add(new TermQuery(new Term("f", "2")), Occur.MUST) + .build(), Occur.MUST) + .build(); + result = analyze(q7, Version.CURRENT); + assertThat(result.minimumShouldMatch, equalTo(3)); + assertThat(result.extractions, hasSize(3)); + assertFalse(result.verified); + assertFalse(result.matchAllDocs); + } + } From 40910251843b7dd72c33ea878b2c2f784552095f Mon Sep 17 00:00:00 2001 From: Przemyslaw Gomulka Date: Tue, 10 Dec 2019 14:22:10 +0100 Subject: [PATCH 140/686] Allow skipping ranges of versions (#50014) Multiple version ranges are allowed to be used in section skip in yml tests. This is useful when a bugfix was backported to latest versions and all previous releases contain a wire breaking bug. examples: 6.1.0 - 6.3.0, 6.6.0 - 6.7.9, 7.0 - - 7.2, 8.0.0 - --- .../search/180_locale_dependent_mapping.yml | 3 - .../test/rest/yaml/section/DoSection.java | 7 ++- .../test/rest/yaml/section/SkipSection.java | 58 ++++++++++--------- .../test/rest/yaml/section/VersionRange.java | 49 ++++++++++++++++ .../rest/yaml/section/SkipSectionTests.java | 21 +++++++ 5 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/180_locale_dependent_mapping.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/180_locale_dependent_mapping.yml index e9ba863675dfa..c4815304e0799 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/180_locale_dependent_mapping.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/180_locale_dependent_mapping.yml @@ -1,8 +1,5 @@ --- "Test Index and Search locale dependent mappings / dates": - - skip: - version: " - 6.1.99" - reason: JDK9 only supports this with a special sysproperty added in 6.2.0 - do: indices.create: index: test_index diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java index ce94adf73bcd3..1b588f554fa53 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java @@ -422,7 +422,7 @@ private static NodeSelector parseVersionSelector(XContentParser parser) throws I if (false == parser.currentToken().isValue()) { throw new XContentParseException(parser.getTokenLocation(), "expected [version] to be a value"); } - Version[] range = SkipSection.parseVersionRange(parser.text()); + List skipVersionRanges = SkipSection.parseVersionRanges(parser.text()); return new NodeSelector() { @Override public void select(Iterable nodes) { @@ -433,7 +433,8 @@ public void select(Iterable nodes) { + node); } Version version = Version.fromString(node.getVersion()); - if (false == (version.onOrAfter(range[0]) && version.onOrBefore(range[1]))) { + boolean skip = skipVersionRanges.stream().anyMatch(v -> v.contains(version)); + if (false == skip) { itr.remove(); } } @@ -441,7 +442,7 @@ public void select(Iterable nodes) { @Override public String toString() { - return "version between [" + range[0] + "] and [" + range[1] + "]"; + return "version ranges "+skipVersionRanges; } }; } diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/SkipSection.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/SkipSection.java index e487f8e74da3b..81eb470892010 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/SkipSection.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/SkipSection.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -98,33 +99,30 @@ public static SkipSection parse(XContentParser parser) throws IOException { public static final SkipSection EMPTY = new SkipSection(); - private final Version lowerVersion; - private final Version upperVersion; + private final List versionRanges; private final List features; private final String reason; private SkipSection() { - this.lowerVersion = null; - this.upperVersion = null; + this.versionRanges = new ArrayList<>(); this.features = new ArrayList<>(); this.reason = null; } public SkipSection(String versionRange, List features, String reason) { assert features != null; - Version[] versions = parseVersionRange(versionRange); - this.lowerVersion = versions[0]; - this.upperVersion = versions[1]; + this.versionRanges = parseVersionRanges(versionRange); + assert versionRanges.isEmpty() == false; this.features = features; this.reason = reason; } public Version getLowerVersion() { - return lowerVersion; + return versionRanges.get(0).getLower(); } public Version getUpperVersion() { - return upperVersion; + return versionRanges.get(versionRanges.size() - 1).getUpper(); } public List getFeatures() { @@ -139,10 +137,8 @@ public boolean skip(Version currentVersion) { if (isEmpty()) { return false; } - boolean skip = lowerVersion != null && upperVersion != null && currentVersion.onOrAfter(lowerVersion) - && currentVersion.onOrBefore(upperVersion); - skip |= Features.areAllSupported(features) == false; - return skip; + boolean skip = versionRanges.stream().anyMatch(range -> range.contains(currentVersion)); + return skip || Features.areAllSupported(features) == false; } public boolean isVersionCheck() { @@ -153,24 +149,30 @@ public boolean isEmpty() { return EMPTY.equals(this); } - static Version[] parseVersionRange(String versionRange) { - if (versionRange == null) { - return new Version[] { null, null }; + static List parseVersionRanges(String rawRanges) { + if (rawRanges == null) { + return Collections.singletonList(new VersionRange(null, null)); } - if (versionRange.trim().equals("all")) { - return new Version[]{VersionUtils.getFirstVersion(), Version.CURRENT}; - } - String[] skipVersions = versionRange.split("-"); - if (skipVersions.length > 2) { - throw new IllegalArgumentException("version range malformed: " + versionRange); + if (rawRanges.trim().equals("all")) { + return Collections.singletonList(new VersionRange(VersionUtils.getFirstVersion(), Version.CURRENT)); } + String[] ranges = rawRanges.split(","); + List versionRanges = new ArrayList<>(); + for (String rawRange : ranges) { + String[] skipVersions = rawRange.split("-", -1); + if (skipVersions.length > 2) { + throw new IllegalArgumentException("version range malformed: " + rawRanges); + } - String lower = skipVersions[0].trim(); - String upper = skipVersions[1].trim(); - return new Version[] { - lower.isEmpty() ? VersionUtils.getFirstVersion() : Version.fromString(lower), - upper.isEmpty() ? Version.CURRENT : Version.fromString(upper) - }; + String lower = skipVersions[0].trim(); + String upper = skipVersions[1].trim(); + VersionRange versionRange = new VersionRange( + lower.isEmpty() ? VersionUtils.getFirstVersion() : Version.fromString(lower), + upper.isEmpty() ? Version.CURRENT : Version.fromString(upper) + ); + versionRanges.add(versionRange); + } + return versionRanges; } public String getSkipMessage(String description) { diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java new file mode 100644 index 0000000000000..f1b1df2a1a167 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java @@ -0,0 +1,49 @@ +/* + * 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. + */ +package org.elasticsearch.test.rest.yaml.section; + +import org.elasticsearch.Version; + +public class VersionRange { + private final Version lower; + private final Version upper; + + public VersionRange(Version lower, Version upper) { + this.lower = lower; + this.upper = upper; + } + + public Version getLower() { + return lower; + } + + public Version getUpper() { + return upper; + } + + public boolean contains(Version currentVersion) { + return lower != null && upper != null && currentVersion.onOrAfter(lower) + && currentVersion.onOrBefore(upper); + } + + @Override + public String toString() { + return "[" + lower + " - " + upper + "]"; + } +} diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SkipSectionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SkipSectionTests.java index e92ef2ce13576..45273912f1d5b 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SkipSectionTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SkipSectionTests.java @@ -33,6 +33,27 @@ public class SkipSectionTests extends AbstractClientYamlTestFragmentParserTestCase { + public void testSkipMultiRange() { + SkipSection section = new SkipSection("6.0.0 - 6.1.0, 7.1.0 - 7.5.0", + Collections.emptyList() , "foobar"); + + assertFalse(section.skip(Version.CURRENT)); + assertFalse(section.skip(Version.fromString("6.2.0"))); + assertFalse(section.skip(Version.fromString("7.0.0"))); + assertFalse(section.skip(Version.fromString("7.6.0"))); + + assertTrue(section.skip(Version.fromString("6.0.0"))); + assertTrue(section.skip(Version.fromString("6.1.0"))); + assertTrue(section.skip(Version.fromString("7.1.0"))); + assertTrue(section.skip(Version.fromString("7.5.0"))); + + section = new SkipSection("- 7.1.0, 7.2.0 - 7.5.0, 8.0.0 -", + Collections.emptyList() , "foobar"); + assertTrue(section.skip(Version.fromString("7.0.0"))); + assertTrue(section.skip(Version.fromString("7.3.0"))); + assertTrue(section.skip(Version.fromString("8.0.0"))); + } + public void testSkip() { SkipSection section = new SkipSection("6.0.0 - 6.1.0", randomBoolean() ? Collections.emptyList() : Collections.singletonList("warnings"), "foobar"); From b362d15b38d0fd7cae676e03cb8ea1b7c1cd9373 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Tue, 10 Dec 2019 09:30:04 -0500 Subject: [PATCH 141/686] [DOCS] Remove shadow replica reference (#50029) Removes a reference to shadow replicas from the cat shards API docs and a comment in cluster/routing/UnassignedInfo.java. Shadow replicas were removed with #23906. --- docs/reference/cat/shards.asciidoc | 2 +- .../java/org/elasticsearch/cluster/routing/UnassignedInfo.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/cat/shards.asciidoc b/docs/reference/cat/shards.asciidoc index 61b1c869f0429..b26472812f514 100644 --- a/docs/reference/cat/shards.asciidoc +++ b/docs/reference/cat/shards.asciidoc @@ -257,7 +257,7 @@ Reason the shard is unassigned. Returned values are: * `NEW_INDEX_RESTORED`: Unassigned as a result of restoring into a new index. * `NODE_LEFT`: Unassigned as a result of the node hosting it leaving the cluster. * `REALLOCATED_REPLICA`: A better replica location is identified and causes the existing replica allocation to be cancelled. -* `REINITIALIZED`: When a shard moves from started back to initializing, for example, with shadow replicas. +* `REINITIALIZED`: When a shard moves from started back to initializing. * `REPLICA_ADDED`: Unassigned as a result of explicit addition of a replica. * `REROUTE_CANCELLED`: Unassigned as a result of explicit cancel reroute command. diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/UnassignedInfo.java b/server/src/main/java/org/elasticsearch/cluster/routing/UnassignedInfo.java index 42b3fde5e0c95..9c862c7a00052 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/UnassignedInfo.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/UnassignedInfo.java @@ -104,7 +104,7 @@ public enum Reason { */ REROUTE_CANCELLED, /** - * When a shard moves from started back to initializing, for example, during shadow replica + * When a shard moves from started back to initializing. */ REINITIALIZED, /** From 7f49890706254ed5391b58cb782ea1453bb5fe33 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 10 Dec 2019 08:03:43 -0800 Subject: [PATCH 142/686] [DOCS] Removes realm type security setting (#50001) --- docs/reference/settings/security-settings.asciidoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index 6fb5084b94a97..e5fc39ea9036a 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -188,7 +188,7 @@ namespace in `elasticsearch.yml`. For example: ---------------------------------------- xpack.security.authc.realms: - native.realm1: + native.realm1: <1> order: 0 ... @@ -201,6 +201,9 @@ xpack.security.authc.realms: ... ... ---------------------------------------- +<1> Specifies the type of realm (for example, `native`, `ldap`, +`active_directory`, `pki`, `file`, `kerberos`, `saml`) and the realm name. This +information is required. The valid settings vary depending on the realm type. For more information, see <>. @@ -209,9 +212,6 @@ information, see <>. [[ref-realm-settings]] ===== Settings valid for all realms -`type`:: -The type of the realm: `native`, `ldap`, `active_directory`, `pki`, or `file`. Required. - `order`:: The priority of the realm within the realm chain. Realms with a lower order are consulted first. Although not required, use of this setting is strongly From 5e2341f7baff1fd56ff38c81792d9c0045032599 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Tue, 10 Dec 2019 17:09:36 +0100 Subject: [PATCH 143/686] Upgrade to lucene 8.4.0-snapshot-662c455. (#50016) Lucene 8.4 is about to be released so we should check it doesn't cause problems with Elasticsearch. --- buildSrc/version.properties | 2 +- .../reference/mapping/params/normalizer.asciidoc | 6 +++--- docs/reference/search/explain.asciidoc | 8 ++++---- docs/reference/search/request-body.asciidoc | 4 ++-- .../search/request/highlighting.asciidoc | 8 ++++---- .../reference/search/request/inner-hits.asciidoc | 8 ++++---- docs/reference/search/search.asciidoc | 4 ++-- docs/reference/search/uri-request.asciidoc | 4 ++-- ...e-expressions-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...pressions-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...analyzers-icu-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...yzers-icu-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...zers-kuromoji-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...-kuromoji-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...nalyzers-nori-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...zers-nori-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...zers-phonetic-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...-phonetic-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...yzers-smartcn-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...s-smartcn-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...yzers-stempel-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...s-stempel-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...rs-morfologik-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...orfologik-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...lyzers-common-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...rs-common-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...ckward-codecs-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...rd-codecs-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - .../lucene-core-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...cene-core-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...cene-grouping-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...-grouping-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...e-highlighter-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...ghlighter-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - .../lucene-join-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...cene-join-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...lucene-memory-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...ne-memory-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - .../lucene-misc-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...cene-misc-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...ucene-queries-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...e-queries-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...e-queryparser-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...eryparser-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...ucene-sandbox-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...e-sandbox-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...ucene-spatial-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...e-spatial-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...patial-extras-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...al-extras-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...ene-spatial3d-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...spatial3d-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - ...ucene-suggest-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...e-suggest-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - .../uhighlight/CustomUnifiedHighlighter.java | 3 +-- .../org/elasticsearch/common/lucene/Lucene.java | 2 +- .../elasticsearch/index/codec/CodecService.java | 6 +++--- .../codec/PerFieldMappingPostingFormatCodec.java | 4 ++-- .../index/mapper/CompletionFieldMapper.java | 4 ++-- .../elasticsearch/indices/IndicesQueryCache.java | 6 +++--- .../elasticsearch/index/codec/CodecTests.java | 16 ++++++++-------- .../lucene-core-8.4.0-snapshot-662c455.jar.sha1 | 1 + ...cene-core-8.4.0-snapshot-e648d601efb.jar.sha1 | 1 - 63 files changed, 66 insertions(+), 67 deletions(-) create mode 100644 modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-analyzers-common-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-analyzers-common-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-backward-codecs-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-backward-codecs-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-core-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-grouping-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-grouping-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-highlighter-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-highlighter-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-join-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-join-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-memory-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-memory-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-misc-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-misc-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-queries-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-queries-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-queryparser-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-queryparser-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-sandbox-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-sandbox-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-spatial-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-spatial-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-spatial-extras-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-spatial-extras-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-spatial3d-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-spatial3d-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 server/licenses/lucene-suggest-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 server/licenses/lucene-suggest-8.4.0-snapshot-e648d601efb.jar.sha1 create mode 100644 x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 delete mode 100644 x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-e648d601efb.jar.sha1 diff --git a/buildSrc/version.properties b/buildSrc/version.properties index 6c7d6798a65c6..ad486276f082e 100644 --- a/buildSrc/version.properties +++ b/buildSrc/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.0.0 -lucene = 8.4.0-snapshot-e648d601efb +lucene = 8.4.0-snapshot-662c455 bundled_jdk_vendor = adoptopenjdk bundled_jdk = 13.0.1+9 diff --git a/docs/reference/mapping/params/normalizer.asciidoc b/docs/reference/mapping/params/normalizer.asciidoc index 1e7e6870c3024..b218d311c7201 100644 --- a/docs/reference/mapping/params/normalizer.asciidoc +++ b/docs/reference/mapping/params/normalizer.asciidoc @@ -90,12 +90,12 @@ both index and query time. "value": 2, "relation": "eq" }, - "max_score": 0.47000363, + "max_score": 0.4700036, "hits": [ { "_index": "index", "_id": "1", - "_score": 0.47000363, + "_score": 0.4700036, "_source": { "foo": "BÀR" } @@ -103,7 +103,7 @@ both index and query time. { "_index": "index", "_id": "2", - "_score": 0.47000363, + "_score": 0.4700036, "_source": { "foo": "bar" } diff --git a/docs/reference/search/explain.asciidoc b/docs/reference/search/explain.asciidoc index a9d431e702284..91654f32adcc8 100644 --- a/docs/reference/search/explain.asciidoc +++ b/docs/reference/search/explain.asciidoc @@ -106,12 +106,12 @@ The API returns the following response: "_id":"0", "matched":true, "explanation":{ - "value":1.6943597, + "value":1.6943598, "description":"weight(message:elasticsearch in 0) [PerFieldSimilarity], result of:", "details":[ { - "value":1.6943597, - "description":"score(freq=1.0), product of:", + "value":1.6943598, + "description":"score(freq=1.0), computed as boost * idf * tf from:", "details":[ { "value":2.2, @@ -135,7 +135,7 @@ The API returns the following response: ] }, { - "value":0.5555555, + "value":0.5555556, "description":"tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:", "details":[ { diff --git a/docs/reference/search/request-body.asciidoc b/docs/reference/search/request-body.asciidoc index d236a83c8eacd..c3a9fe71e16b8 100644 --- a/docs/reference/search/request-body.asciidoc +++ b/docs/reference/search/request-body.asciidoc @@ -118,12 +118,12 @@ The API returns the following response: "value": 1, "relation": "eq" }, - "max_score": 1.3862944, + "max_score": 1.3862942, "hits" : [ { "_index" : "twitter", "_id" : "0", - "_score": 1.3862944, + "_score": 1.3862942, "_source" : { "user" : "kimchy", "message": "trying out Elasticsearch", diff --git a/docs/reference/search/request/highlighting.asciidoc b/docs/reference/search/request/highlighting.asciidoc index e8171d43b17f1..cb9b84ad3378f 100644 --- a/docs/reference/search/request/highlighting.asciidoc +++ b/docs/reference/search/request/highlighting.asciidoc @@ -840,12 +840,12 @@ Response: "value": 1, "relation": "eq" }, - "max_score": 1.601195, + "max_score": 1.6011951, "hits": [ { "_index": "twitter", "_id": "1", - "_score": 1.601195, + "_score": 1.6011951, "_source": { "user": "test", "message": "some message with the number 1", @@ -897,12 +897,12 @@ Response: "value": 1, "relation": "eq" }, - "max_score": 1.601195, + "max_score": 1.6011951, "hits": [ { "_index": "twitter", "_id": "1", - "_score": 1.601195, + "_score": 1.6011951, "_source": { "user": "test", "message": "some message with the number 1", diff --git a/docs/reference/search/request/inner-hits.asciidoc b/docs/reference/search/request/inner-hits.asciidoc index b356c2cfc2d7c..53ae303e484a8 100644 --- a/docs/reference/search/request/inner-hits.asciidoc +++ b/docs/reference/search/request/inner-hits.asciidoc @@ -379,12 +379,12 @@ Which would look like: "value": 1, "relation": "eq" }, - "max_score": 0.6931472, + "max_score": 0.6931471, "hits": [ { "_index": "test", "_id": "1", - "_score": 0.6931472, + "_score": 0.6931471, "_source": ..., "inner_hits": { "comments.votes": { <1> @@ -393,7 +393,7 @@ Which would look like: "value": 1, "relation": "eq" }, - "max_score": 0.6931472, + "max_score": 0.6931471, "hits": [ { "_index": "test", @@ -406,7 +406,7 @@ Which would look like: "offset": 0 } }, - "_score": 0.6931472, + "_score": 0.6931471, "_source": { "value": 1, "voter": "kimchy" diff --git a/docs/reference/search/search.asciidoc b/docs/reference/search/search.asciidoc index 85c97f6c74d5a..ccec2345a0e5f 100644 --- a/docs/reference/search/search.asciidoc +++ b/docs/reference/search/search.asciidoc @@ -360,12 +360,12 @@ The API returns the following response: "value" : 1, "relation" : "eq" }, - "max_score" : 1.3862944, + "max_score" : 1.3862942, "hits" : [ { "_index" : "twitter", "_id" : "0", - "_score" : 1.3862944, + "_score" : 1.3862942, "_source" : { "date" : "2009-11-15T14:12:12", "likes" : 0, diff --git a/docs/reference/search/uri-request.asciidoc b/docs/reference/search/uri-request.asciidoc index ff234f415a3d0..695c4a6ada111 100644 --- a/docs/reference/search/uri-request.asciidoc +++ b/docs/reference/search/uri-request.asciidoc @@ -134,12 +134,12 @@ The API returns the following response: "value": 1, "relation": "eq" }, - "max_score": 1.3862944, + "max_score": 1.3862942, "hits" : [ { "_index" : "twitter", "_id" : "0", - "_score": 1.3862944, + "_score": 1.3862942, "_source" : { "user" : "kimchy", "date" : "2009-11-15T14:12:12", diff --git a/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-662c455.jar.sha1 b/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..1c4c5ce2b62db --- /dev/null +++ b/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +4041db9db7c394584571b45812734732912ef8e2 \ No newline at end of file diff --git a/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-e648d601efb.jar.sha1 b/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 7a75661f63f65..0000000000000 --- a/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -43b9178f582373f4fcee61837404c0cc8636043e \ No newline at end of file diff --git a/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..0fc96bc500eff --- /dev/null +++ b/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +d5bddd6b7660439e29bbce26ded283931c756d75 \ No newline at end of file diff --git a/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-e648d601efb.jar.sha1 b/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 2765cfafb0520..0000000000000 --- a/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -8ee342fa6e6306e56b583251639a661250fada46 \ No newline at end of file diff --git a/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..388bc9748b7f0 --- /dev/null +++ b/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +4303858c346c51bbbc68c32eb25f7f372b09331c \ No newline at end of file diff --git a/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-e648d601efb.jar.sha1 b/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index f653bf5c3b5dc..0000000000000 --- a/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -7e31f2a38d1434eb50781efc65b0e028f08d7821 \ No newline at end of file diff --git a/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..07ff7fd907a22 --- /dev/null +++ b/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +b1a9182ed1b92a121c1587fe9710aa7a41f3f77a \ No newline at end of file diff --git a/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-e648d601efb.jar.sha1 b/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 2c3ee0313a9c7..0000000000000 --- a/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -9079d81a8ea2c7190ef09ca06a987d1cab2fdf17 \ No newline at end of file diff --git a/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..95e603ec18882 --- /dev/null +++ b/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +4df747b25286baecf5e790bf76bc40038c059691 \ No newline at end of file diff --git a/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-e648d601efb.jar.sha1 b/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 5de2626b6ad2e..0000000000000 --- a/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -f253f59d4e8bb6e55eb307b011ddb81ba0ebab92 \ No newline at end of file diff --git a/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..4eaf91f308397 --- /dev/null +++ b/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +88d3f8f9134b95884f3b80280b09aa2513b71297 \ No newline at end of file diff --git a/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-e648d601efb.jar.sha1 b/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index fcb579806bfeb..0000000000000 --- a/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -36547378493e6e84f63dc744df8d414cb2add1a4 \ No newline at end of file diff --git a/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..e28b8d87cd559 --- /dev/null +++ b/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +9ddccf575ee03a1329c8d1eb2e4ee7a6e3f3f56f \ No newline at end of file diff --git a/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-e648d601efb.jar.sha1 b/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index d26f99ab24e7b..0000000000000 --- a/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -8b15a376efa7d4289b697144f34a819a9f8772f1 \ No newline at end of file diff --git a/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..1b8ec8c5831cb --- /dev/null +++ b/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +e115e562a42c12a3292fb138607855c1fdfb0772 \ No newline at end of file diff --git a/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-e648d601efb.jar.sha1 b/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 43a7650c70d7a..0000000000000 --- a/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -d1bc4170e6981ca9af71d7a4ce46a3feb2f7b613 \ No newline at end of file diff --git a/server/licenses/lucene-analyzers-common-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-analyzers-common-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..d6f8049f7b1e1 --- /dev/null +++ b/server/licenses/lucene-analyzers-common-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +061fb94ab616492721f8868dcaec3fbc989733be \ No newline at end of file diff --git a/server/licenses/lucene-analyzers-common-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-analyzers-common-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index c2ec1128c1741..0000000000000 --- a/server/licenses/lucene-analyzers-common-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -1cb225781b19e758d216987e363b77fa4b041174 \ No newline at end of file diff --git a/server/licenses/lucene-backward-codecs-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-backward-codecs-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..243c4420beabe --- /dev/null +++ b/server/licenses/lucene-backward-codecs-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +503f3d516889a99e1c0e2dbdba7bf9cc9900c54c \ No newline at end of file diff --git a/server/licenses/lucene-backward-codecs-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-backward-codecs-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index b6486fb3eeba7..0000000000000 --- a/server/licenses/lucene-backward-codecs-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -cbbf849e24ef0cc61312579acf6d6c5b72c99cf5 \ No newline at end of file diff --git a/server/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..d1657fccc5ee2 --- /dev/null +++ b/server/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +8ca36adea0a904ec725d57f509a62652a53ecff8 \ No newline at end of file diff --git a/server/licenses/lucene-core-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-core-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 4b736046f3ade..0000000000000 --- a/server/licenses/lucene-core-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -aa74590851b6fcf536976f75448be52f6ca18a4a \ No newline at end of file diff --git a/server/licenses/lucene-grouping-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-grouping-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..f1f0684d9b389 --- /dev/null +++ b/server/licenses/lucene-grouping-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +f176fdcf8fc574f4cb1c549aaa4da0301afd34ba \ No newline at end of file diff --git a/server/licenses/lucene-grouping-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-grouping-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 97a3c7b927b87..0000000000000 --- a/server/licenses/lucene-grouping-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -1bd113010c183168d79fbc10a6b590fdacc3fa35 \ No newline at end of file diff --git a/server/licenses/lucene-highlighter-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-highlighter-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..a9ad6fb95cb8b --- /dev/null +++ b/server/licenses/lucene-highlighter-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +db5ea7b647309e5d29fa92bcbb6b11286d11436d \ No newline at end of file diff --git a/server/licenses/lucene-highlighter-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-highlighter-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index f2dd654d8d64c..0000000000000 --- a/server/licenses/lucene-highlighter-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -4e44a435e14d12113ca9193182a302677fda155e \ No newline at end of file diff --git a/server/licenses/lucene-join-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-join-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..6ef1d079f63f1 --- /dev/null +++ b/server/licenses/lucene-join-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +36329bc2ea6a5640d4128206221456656de7bbe2 \ No newline at end of file diff --git a/server/licenses/lucene-join-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-join-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 9e8d72cc13fcf..0000000000000 --- a/server/licenses/lucene-join-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -eb8eacd015ef81ef2055ada357a92c9751308ef1 \ No newline at end of file diff --git a/server/licenses/lucene-memory-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-memory-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..eeb424851022e --- /dev/null +++ b/server/licenses/lucene-memory-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +083f492781b3d2c1d470bd1439c875ebf74a14eb \ No newline at end of file diff --git a/server/licenses/lucene-memory-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-memory-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index e6048ffd91225..0000000000000 --- a/server/licenses/lucene-memory-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -4dc565203bb1eab0222c52215891e207e7032209 \ No newline at end of file diff --git a/server/licenses/lucene-misc-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-misc-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..6f5d479c76d64 --- /dev/null +++ b/server/licenses/lucene-misc-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +9cd5ea7bc08d93053ca993bd6fc1c9cd0a1b91fd \ No newline at end of file diff --git a/server/licenses/lucene-misc-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-misc-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 480dcc632907f..0000000000000 --- a/server/licenses/lucene-misc-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -ef596e6d2a7ac9c7dfc6196dad75dc719c81ce85 \ No newline at end of file diff --git a/server/licenses/lucene-queries-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-queries-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..30733a5a57764 --- /dev/null +++ b/server/licenses/lucene-queries-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +89e39f65d1c42b5849ccf3a8e6cc9b3b277c08a6 \ No newline at end of file diff --git a/server/licenses/lucene-queries-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-queries-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 2524672e062b1..0000000000000 --- a/server/licenses/lucene-queries-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -b0c963e68dd71444f09336258c8f63425514426a \ No newline at end of file diff --git a/server/licenses/lucene-queryparser-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-queryparser-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..98b065176a418 --- /dev/null +++ b/server/licenses/lucene-queryparser-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +651f6a0075ee30b814c8b56020d95155424c0e67 \ No newline at end of file diff --git a/server/licenses/lucene-queryparser-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-queryparser-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 4ab7a7fe6f644..0000000000000 --- a/server/licenses/lucene-queryparser-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -bfab3e9b0467662a8ff969da215dc4a999b73076 \ No newline at end of file diff --git a/server/licenses/lucene-sandbox-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-sandbox-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..484ce6b5c00f0 --- /dev/null +++ b/server/licenses/lucene-sandbox-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +935968488cc2bbcd3ced9c254f690e7c90447d9e \ No newline at end of file diff --git a/server/licenses/lucene-sandbox-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-sandbox-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 9361e9252f212..0000000000000 --- a/server/licenses/lucene-sandbox-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -dadfc90e4cd032f8a4db5cc1e0bdddecea635edb \ No newline at end of file diff --git a/server/licenses/lucene-spatial-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-spatial-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..1bb42417cb147 --- /dev/null +++ b/server/licenses/lucene-spatial-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +0bbdd0002d8d87e54b5caff6c77a1627bf449d38 \ No newline at end of file diff --git a/server/licenses/lucene-spatial-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-spatial-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index ce5a13ec8d6be..0000000000000 --- a/server/licenses/lucene-spatial-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -e72dd79d30781e4d05bc8397ae61d0b51d7ad522 \ No newline at end of file diff --git a/server/licenses/lucene-spatial-extras-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-spatial-extras-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..2bdbd889b4454 --- /dev/null +++ b/server/licenses/lucene-spatial-extras-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +255b547571dcec118ff1a0560bb16e259f96b76a \ No newline at end of file diff --git a/server/licenses/lucene-spatial-extras-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-spatial-extras-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 4530b17e84e25..0000000000000 --- a/server/licenses/lucene-spatial-extras-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -e6b6dbd0526287f25d98d7fe354d5e290c875b8a \ No newline at end of file diff --git a/server/licenses/lucene-spatial3d-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-spatial3d-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..e7036243119a9 --- /dev/null +++ b/server/licenses/lucene-spatial3d-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +739af6d9876f6aa7f2a3d46fa3f236a5d6ee3653 \ No newline at end of file diff --git a/server/licenses/lucene-spatial3d-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-spatial3d-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index a96977cf1340f..0000000000000 --- a/server/licenses/lucene-spatial3d-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -6351edfc6dde2aefd8f6d8ef33ae5a6e08f88321 \ No newline at end of file diff --git a/server/licenses/lucene-suggest-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-suggest-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..72c92c101b050 --- /dev/null +++ b/server/licenses/lucene-suggest-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +20fa11a541a7ca3a50caa443a9abf0276b1194ea \ No newline at end of file diff --git a/server/licenses/lucene-suggest-8.4.0-snapshot-e648d601efb.jar.sha1 b/server/licenses/lucene-suggest-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 090cf9ee734ca..0000000000000 --- a/server/licenses/lucene-suggest-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -921dd4ab493b9d70a0b1bf7b0fe8a6790b7e8036 \ No newline at end of file diff --git a/server/src/main/java/org/apache/lucene/search/uhighlight/CustomUnifiedHighlighter.java b/server/src/main/java/org/apache/lucene/search/uhighlight/CustomUnifiedHighlighter.java index 2d35de522b5f0..db79122fa3da7 100644 --- a/server/src/main/java/org/apache/lucene/search/uhighlight/CustomUnifiedHighlighter.java +++ b/server/src/main/java/org/apache/lucene/search/uhighlight/CustomUnifiedHighlighter.java @@ -31,7 +31,6 @@ import org.apache.lucene.search.spans.SpanQuery; import org.apache.lucene.search.spans.SpanTermQuery; import org.apache.lucene.util.BytesRef; -import org.apache.lucene.util.automaton.CharacterRunAutomaton; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.lucene.search.MultiPhrasePrefixQuery; @@ -136,7 +135,7 @@ protected FieldHighlighter getFieldHighlighter(String field, Query query, Set highlightFlags = getFlags(field); PhraseHelper phraseHelper = getPhraseHelper(field, query, highlightFlags); - CharacterRunAutomaton[] automata = getAutomata(field, query, highlightFlags); + LabelledCharArrayMatcher[] automata = getAutomata(field, query, highlightFlags); UHComponents components = new UHComponents(field, fieldMatcher, query, terms, phraseHelper, automata, false , highlightFlags); OffsetSource offsetSource = getOptimizedOffsetSource(components); BreakIterator breakIterator = new SplittingBreakIterator(getBreakIterator(field), diff --git a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java index fcab160108b29..ce00c7755205f 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java @@ -107,7 +107,7 @@ public class Lucene { public static final String LATEST_DOC_VALUES_FORMAT = "Lucene70"; public static final String LATEST_POSTINGS_FORMAT = "Lucene50"; - public static final String LATEST_CODEC = "Lucene80"; + public static final String LATEST_CODEC = "Lucene84"; static { Deprecated annotation = PostingsFormat.forName(LATEST_POSTINGS_FORMAT).getClass().getAnnotation(Deprecated.class); diff --git a/server/src/main/java/org/elasticsearch/index/codec/CodecService.java b/server/src/main/java/org/elasticsearch/index/codec/CodecService.java index 485c40d5d9bbd..0b1c96a6911d9 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/CodecService.java +++ b/server/src/main/java/org/elasticsearch/index/codec/CodecService.java @@ -22,7 +22,7 @@ import org.apache.logging.log4j.Logger; import org.apache.lucene.codecs.Codec; import org.apache.lucene.codecs.lucene50.Lucene50StoredFieldsFormat.Mode; -import org.apache.lucene.codecs.lucene80.Lucene80Codec; +import org.apache.lucene.codecs.lucene84.Lucene84Codec; import org.elasticsearch.common.Nullable; import org.elasticsearch.index.mapper.MapperService; @@ -47,8 +47,8 @@ public class CodecService { public CodecService(@Nullable MapperService mapperService, Logger logger) { final var codecs = new HashMap(); if (mapperService == null) { - codecs.put(DEFAULT_CODEC, new Lucene80Codec()); - codecs.put(BEST_COMPRESSION_CODEC, new Lucene80Codec(Mode.BEST_COMPRESSION)); + codecs.put(DEFAULT_CODEC, new Lucene84Codec()); + codecs.put(BEST_COMPRESSION_CODEC, new Lucene84Codec(Mode.BEST_COMPRESSION)); } else { codecs.put(DEFAULT_CODEC, new PerFieldMappingPostingFormatCodec(Mode.BEST_SPEED, mapperService, logger)); diff --git a/server/src/main/java/org/elasticsearch/index/codec/PerFieldMappingPostingFormatCodec.java b/server/src/main/java/org/elasticsearch/index/codec/PerFieldMappingPostingFormatCodec.java index 4a154abd8eadd..ccaa873af2790 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/PerFieldMappingPostingFormatCodec.java +++ b/server/src/main/java/org/elasticsearch/index/codec/PerFieldMappingPostingFormatCodec.java @@ -23,7 +23,7 @@ import org.apache.lucene.codecs.Codec; import org.apache.lucene.codecs.PostingsFormat; import org.apache.lucene.codecs.lucene50.Lucene50StoredFieldsFormat; -import org.apache.lucene.codecs.lucene80.Lucene80Codec; +import org.apache.lucene.codecs.lucene84.Lucene84Codec; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.index.mapper.CompletionFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; @@ -37,7 +37,7 @@ * per index in real time via the mapping API. If no specific postings format is * configured for a specific field the default postings format is used. */ -public class PerFieldMappingPostingFormatCodec extends Lucene80Codec { +public class PerFieldMappingPostingFormatCodec extends Lucene84Codec { private final Logger logger; private final MapperService mapperService; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java index 5fd06633bcfc8..5f6b71d6522c2 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java @@ -24,7 +24,7 @@ import org.apache.lucene.index.Term; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; -import org.apache.lucene.search.suggest.document.Completion50PostingsFormat; +import org.apache.lucene.search.suggest.document.Completion84PostingsFormat; import org.apache.lucene.search.suggest.document.CompletionAnalyzer; import org.apache.lucene.search.suggest.document.CompletionQuery; import org.apache.lucene.search.suggest.document.FuzzyCompletionQuery; @@ -265,7 +265,7 @@ public boolean preservePositionIncrements() { */ public static synchronized PostingsFormat postingsFormat() { if (postingsFormat == null) { - postingsFormat = new Completion50PostingsFormat(); + postingsFormat = new Completion84PostingsFormat(); } return postingsFormat; } diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesQueryCache.java b/server/src/main/java/org/elasticsearch/indices/IndicesQueryCache.java index dc054f8b51d3e..9183b1a826563 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesQueryCache.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesQueryCache.java @@ -80,7 +80,7 @@ public IndicesQueryCache(Settings settings) { logger.debug("using [node] query cache with size [{}] max filter count [{}]", size, count); if (INDICES_QUERIES_CACHE_ALL_SEGMENTS_SETTING.get(settings)) { - cache = new ElasticsearchLRUQueryCache(count, size.getBytes(), context -> true); + cache = new ElasticsearchLRUQueryCache(count, size.getBytes(), context -> true, 1f); } else { cache = new ElasticsearchLRUQueryCache(count, size.getBytes()); } @@ -250,8 +250,8 @@ public void onClose(ShardId shardId) { private class ElasticsearchLRUQueryCache extends LRUQueryCache { - ElasticsearchLRUQueryCache(int maxSize, long maxRamBytesUsed, Predicate leavesToCache) { - super(maxSize, maxRamBytesUsed, leavesToCache); + ElasticsearchLRUQueryCache(int maxSize, long maxRamBytesUsed, Predicate leavesToCache, float skipFactor) { + super(maxSize, maxRamBytesUsed, leavesToCache, skipFactor); } ElasticsearchLRUQueryCache(int maxSize, long maxRamBytesUsed) { diff --git a/server/src/test/java/org/elasticsearch/index/codec/CodecTests.java b/server/src/test/java/org/elasticsearch/index/codec/CodecTests.java index fa775a84c72ad..dc5b8031a6c50 100644 --- a/server/src/test/java/org/elasticsearch/index/codec/CodecTests.java +++ b/server/src/test/java/org/elasticsearch/index/codec/CodecTests.java @@ -19,11 +19,16 @@ package org.elasticsearch.index.codec; +import static org.hamcrest.Matchers.instanceOf; + +import java.io.IOException; +import java.util.Collections; + import org.apache.logging.log4j.LogManager; import org.apache.lucene.codecs.Codec; import org.apache.lucene.codecs.lucene50.Lucene50StoredFieldsFormat; import org.apache.lucene.codecs.lucene50.Lucene50StoredFieldsFormat.Mode; -import org.apache.lucene.codecs.lucene80.Lucene80Codec; +import org.apache.lucene.codecs.lucene84.Lucene84Codec; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexWriter; @@ -42,19 +47,14 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.IndexSettingsModule; -import java.io.IOException; -import java.util.Collections; - -import static org.hamcrest.Matchers.instanceOf; - @SuppressCodecs("*") // we test against default codec so never get a random one here! public class CodecTests extends ESTestCase { public void testResolveDefaultCodecs() throws Exception { CodecService codecService = createCodecService(); assertThat(codecService.codec("default"), instanceOf(PerFieldMappingPostingFormatCodec.class)); - assertThat(codecService.codec("default"), instanceOf(Lucene80Codec.class)); - assertThat(codecService.codec("Lucene80"), instanceOf(Lucene80Codec.class)); + assertThat(codecService.codec("default"), instanceOf(Lucene84Codec.class)); + assertThat(codecService.codec("Lucene84"), instanceOf(Lucene84Codec.class)); } public void testDefault() throws Exception { diff --git a/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 b/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 new file mode 100644 index 0000000000000..d1657fccc5ee2 --- /dev/null +++ b/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 @@ -0,0 +1 @@ +8ca36adea0a904ec725d57f509a62652a53ecff8 \ No newline at end of file diff --git a/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-e648d601efb.jar.sha1 b/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-e648d601efb.jar.sha1 deleted file mode 100644 index 4b736046f3ade..0000000000000 --- a/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-e648d601efb.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -aa74590851b6fcf536976f75448be52f6ca18a4a \ No newline at end of file From d6603c26d5a6d11ca1f39a5af988659e192522ff Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Tue, 10 Dec 2019 17:32:42 +0100 Subject: [PATCH 144/686] =?UTF-8?q?[Docs]=C2=A0Fix=20typo=20in=20function-?= =?UTF-8?q?score-query.asciidoc=20(#50030)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reference/query-dsl/function-score-query.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/query-dsl/function-score-query.asciidoc b/docs/reference/query-dsl/function-score-query.asciidoc index 371ba5e638145..c4924ebd36727 100644 --- a/docs/reference/query-dsl/function-score-query.asciidoc +++ b/docs/reference/query-dsl/function-score-query.asciidoc @@ -415,7 +415,7 @@ GET /_search `offset`:: If an `offset` is defined, the decay function will only compute the - decay function for documents with a distance greater that the defined + decay function for documents with a distance greater than the defined `offset`. The default is 0. `decay`:: From 91e4f61b4267cbcd8a73db315b5269308c8e5948 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Tue, 10 Dec 2019 14:18:33 -0500 Subject: [PATCH 145/686] muting test UUIDTests.testCompression (#50050) --- server/src/test/java/org/elasticsearch/common/UUIDTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/common/UUIDTests.java b/server/src/test/java/org/elasticsearch/common/UUIDTests.java index dcc440acbcd1a..1d23570064fe5 100644 --- a/server/src/test/java/org/elasticsearch/common/UUIDTests.java +++ b/server/src/test/java/org/elasticsearch/common/UUIDTests.java @@ -116,6 +116,7 @@ public void testUUIDThreaded(UUIDGenerator uuidSource) { assertEquals(count*uuids, globalSet.size()); } + @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/50048") public void testCompression() throws Exception { Logger logger = LogManager.getLogger(UUIDTests.class); // Low number so that the test runs quickly, but the results are more interesting with larger numbers From cd828cc85922ab30137605f1a0c2183f51451bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Krawaczy=C5=84ski?= Date: Tue, 10 Dec 2019 20:23:14 +0100 Subject: [PATCH 146/686] [DOCS] Document `index.queries.cache.enabled` as a static setting (#49886) --- docs/reference/modules/indices/query_cache.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/modules/indices/query_cache.asciidoc b/docs/reference/modules/indices/query_cache.asciidoc index aaa1ab1742841..b61c2a6eee046 100644 --- a/docs/reference/modules/indices/query_cache.asciidoc +++ b/docs/reference/modules/indices/query_cache.asciidoc @@ -18,7 +18,8 @@ the cluster: either a percentage value, like `5%`, or an exact value, like `512mb`. The following setting is an _index_ setting that can be configured on a -per-index basis: +per-index basis. Can only be set at index creation time or on a +<>: `index.queries.cache.enabled`:: From 48769be5bb46ff0400c78e1da06c2f5ea35d1791 Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Tue, 10 Dec 2019 12:55:06 -0800 Subject: [PATCH 147/686] Update Painless AST Catch Node (#50044) This makes two changes to the catch node: 1. Use SDeclaration to replace independent variable usage. 2. Use a DType to set a "minimum" exception type - this allows us to require users to continue using Exception as "minimum" type for catch blocks, but for us to internally catch Error/Throwable. This is a required step to removing custom try/catch blocks from SClass. --- .../elasticsearch/painless/antlr/Walker.java | 8 +++- .../painless/node/DResolvedType.java | 2 +- .../elasticsearch/painless/node/SCatch.java | 38 +++++++++---------- .../painless/node/SDeclaration.java | 2 +- .../painless/node/NodeToStringTests.java | 14 ++++--- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java index 53c98f7589ef3..9b4c52dad77b8 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java @@ -109,6 +109,7 @@ import org.elasticsearch.painless.node.AExpression; import org.elasticsearch.painless.node.ANode; import org.elasticsearch.painless.node.AStatement; +import org.elasticsearch.painless.node.DResolvedType; import org.elasticsearch.painless.node.DUnresolvedType; import org.elasticsearch.painless.node.EAssignment; import org.elasticsearch.painless.node.EBinary; @@ -232,6 +233,10 @@ private Location location(ParserRuleContext ctx) { return new Location(sourceName, ctx.getStart().getStartIndex()); } + private Location location(TerminalNode tn) { + return new Location(sourceName, tn.getSymbol().getStartIndex()); + } + @Override public ANode visitSource(SourceContext ctx) { List functions = new ArrayList<>(); @@ -503,7 +508,8 @@ public ANode visitTrap(TrapContext ctx) { String name = ctx.ID().getText(); SBlock block = (SBlock)visit(ctx.block()); - return new SCatch(location(ctx), type, name, block); + return new SCatch(location(ctx), new DResolvedType(location(ctx), Exception.class), + new SDeclaration(location(ctx.TYPE()), new DUnresolvedType(location(ctx.TYPE()), type), name, null), block); } @Override diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DResolvedType.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DResolvedType.java index 223b39068673d..c1917944f260e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DResolvedType.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/DResolvedType.java @@ -76,6 +76,6 @@ public Class getType() { @Override public String toString() { - return " (DResolvedType [" + PainlessLookupUtility.typeToCanonicalTypeName(type) + "])"; + return "(DResolvedType [" + PainlessLookupUtility.typeToCanonicalTypeName(type) + "])"; } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SCatch.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SCatch.java index ae5e421afa18b..9fc6dc29fe217 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SCatch.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SCatch.java @@ -22,10 +22,10 @@ import org.elasticsearch.painless.ClassWriter; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; -import org.elasticsearch.painless.Locals.Variable; import org.elasticsearch.painless.Location; import org.elasticsearch.painless.MethodWriter; import org.elasticsearch.painless.ScriptRoot; +import org.elasticsearch.painless.lookup.PainlessLookupUtility; import org.objectweb.asm.Label; import org.objectweb.asm.Opcodes; @@ -37,27 +37,25 @@ */ public final class SCatch extends AStatement { - private final String type; - private final String name; + private final DType baseException; + private final SDeclaration declaration; private final SBlock block; - private Variable variable = null; - Label begin = null; Label end = null; Label exception = null; - public SCatch(Location location, String type, String name, SBlock block) { + public SCatch(Location location, DType baseException, SDeclaration declaration, SBlock block) { super(location); - this.type = Objects.requireNonNull(type); - this.name = Objects.requireNonNull(name); + this.baseException = Objects.requireNonNull(baseException); + this.declaration = Objects.requireNonNull(declaration); this.block = block; } @Override void extractVariables(Set variables) { - variables.add(name); + declaration.extractVariables(variables); if (block != null) { block.extractVariables(variables); @@ -66,18 +64,17 @@ void extractVariables(Set variables) { @Override void analyze(ScriptRoot scriptRoot, Locals locals) { - Class clazz = scriptRoot.getPainlessLookup().canonicalTypeNameToType(this.type); + declaration.analyze(scriptRoot, locals); - if (clazz == null) { - throw createError(new IllegalArgumentException("Not a type [" + this.type + "].")); - } + Class baseType = baseException.resolveType(scriptRoot.getPainlessLookup()).getType(); + Class type = declaration.variable.clazz; - if (!Exception.class.isAssignableFrom(clazz)) { - throw createError(new ClassCastException("Not an exception type [" + this.type + "].")); + if (baseType.isAssignableFrom(type) == false) { + throw createError(new ClassCastException( + "cannot cast from [" + PainlessLookupUtility.typeToCanonicalTypeName(type) + "] " + + "to [" + PainlessLookupUtility.typeToCanonicalTypeName(baseType) + "]")); } - variable = locals.addVariable(location, clazz, name, true); - if (block != null) { block.lastSource = lastSource; block.inLoop = inLoop; @@ -100,7 +97,8 @@ void write(ClassWriter classWriter, MethodWriter methodWriter, Globals globals) Label jump = new Label(); methodWriter.mark(jump); - methodWriter.visitVarInsn(MethodWriter.getType(variable.clazz).getOpcode(Opcodes.ISTORE), variable.getSlot()); + methodWriter.visitVarInsn( + MethodWriter.getType(declaration.variable.clazz).getOpcode(Opcodes.ISTORE), declaration.variable.getSlot()); if (block != null) { block.continu = continu; @@ -108,7 +106,7 @@ void write(ClassWriter classWriter, MethodWriter methodWriter, Globals globals) block.write(classWriter, methodWriter, globals); } - methodWriter.visitTryCatchBlock(begin, end, jump, MethodWriter.getType(variable.clazz).getInternalName()); + methodWriter.visitTryCatchBlock(begin, end, jump, MethodWriter.getType(declaration.variable.clazz).getInternalName()); if (exception != null && (block == null || !block.allEscape)) { methodWriter.goTo(exception); @@ -117,6 +115,6 @@ void write(ClassWriter classWriter, MethodWriter methodWriter, Globals globals) @Override public String toString() { - return singleLineToString(type, name, block); + return singleLineToString(baseException, declaration, block); } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java index e5d8f1e881174..bcc2036aaffd4 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java @@ -40,7 +40,7 @@ public final class SDeclaration extends AStatement { private final String name; private AExpression expression; - private Variable variable = null; + Variable variable = null; public SDeclaration(Location location, DType type, String name, AExpression expression) { super(location); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/node/NodeToStringTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/node/NodeToStringTests.java index 562b6e1e5e90c..60310ab0c4cd3 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/node/NodeToStringTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/node/NodeToStringTests.java @@ -853,7 +853,8 @@ public void testSFunction() { public void testSTryAndSCatch() { assertToString( "(SClass (STry (SBlock (SReturn (ENumeric 1)))\n" - + " (SCatch Exception e (SBlock (SReturn (ENumeric 2))))))", + + " (SCatch (DResolvedType [java.lang.Exception]) (SDeclaration (DUnresolvedType [Exception]) e) " + + "(SBlock (SReturn (ENumeric 2))))))", "try {\n" + " return 1\n" + "} catch (Exception e) {\n" @@ -863,7 +864,8 @@ public void testSTryAndSCatch() { "(SClass (STry (SBlock\n" + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) i (ENumeric 1)))\n" + " (SReturn (ENumeric 1)))\n" - + " (SCatch Exception e (SBlock (SReturn (ENumeric 2))))))", + + " (SCatch (DResolvedType [java.lang.Exception]) (SDeclaration (DUnresolvedType [Exception]) e) " + + "(SBlock (SReturn (ENumeric 2))))))", "try {\n" + " int i = 1;" + " return 1\n" @@ -872,7 +874,7 @@ public void testSTryAndSCatch() { + "}"); assertToString( "(SClass (STry (SBlock (SReturn (ENumeric 1)))\n" - + " (SCatch Exception e (SBlock\n" + + " (SCatch (DResolvedType [java.lang.Exception]) (SDeclaration (DUnresolvedType [Exception]) e) (SBlock\n" + " (SDeclBlock (SDeclaration (DUnresolvedType [int]) i (ENumeric 1)))\n" + " (SReturn (ENumeric 2))))))", "try {\n" @@ -883,8 +885,10 @@ public void testSTryAndSCatch() { + "}"); assertToString( "(SClass (STry (SBlock (SReturn (ENumeric 1)))\n" - + " (SCatch NullPointerException e (SBlock (SReturn (ENumeric 2))))\n" - + " (SCatch Exception e (SBlock (SReturn (ENumeric 3))))))", + + " (SCatch (DResolvedType [java.lang.Exception]) (SDeclaration (DUnresolvedType [NullPointerException]) e) " + + "(SBlock (SReturn (ENumeric 2))))\n" + + " (SCatch (DResolvedType [java.lang.Exception]) (SDeclaration (DUnresolvedType [Exception]) e) " + + "(SBlock (SReturn (ENumeric 3))))))", "try {\n" + " return 1\n" + "} catch (NullPointerException e) {\n" From b24d5b4f08a8644db60cc1d6015ef45b4e207d47 Mon Sep 17 00:00:00 2001 From: Stuart Cam Date: Wed, 11 Dec 2019 13:23:00 +1100 Subject: [PATCH 148/686] Add the REST API specifications for SLM Status / Start / Stop endpoints. (#49759) Was originally missed in PR #47710 --- .../rest-api-spec/api/slm.get_status.json | 19 +++++++++++++++++++ .../rest-api-spec/api/slm.start.json | 19 +++++++++++++++++++ .../resources/rest-api-spec/api/slm.stop.json | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/slm.get_status.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/slm.start.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/slm.stop.json diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/slm.get_status.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/slm.get_status.json new file mode 100644 index 0000000000000..163ad5558c3d9 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/slm.get_status.json @@ -0,0 +1,19 @@ +{ + "slm.get_status":{ + "documentation":{ + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/slm-get-status.html" + }, + "stability":"stable", + "url":{ + "paths":[ + { + "path":"/_slm/status", + "methods":[ + "GET" + ] + } + ] + }, + "params":{} + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/slm.start.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/slm.start.json new file mode 100644 index 0000000000000..21ae3d5097862 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/slm.start.json @@ -0,0 +1,19 @@ +{ + "slm.start":{ + "documentation":{ + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/slm-start.html" + }, + "stability":"stable", + "url":{ + "paths":[ + { + "path":"/_slm/start", + "methods":[ + "POST" + ] + } + ] + }, + "params":{} + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/slm.stop.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/slm.stop.json new file mode 100644 index 0000000000000..63b74ab9c2f7e --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/slm.stop.json @@ -0,0 +1,19 @@ +{ + "slm.stop":{ + "documentation":{ + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/slm-stop.html" + }, + "stability":"stable", + "url":{ + "paths":[ + { + "path":"/_slm/stop", + "methods":[ + "POST" + ] + } + ] + }, + "params":{} + } +} From d8d90b62a7aadd69c6cfbfd03d184a8b3fd1d526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Wed, 11 Dec 2019 08:50:17 +0100 Subject: [PATCH 149/686] A few improvements to AnalyticsProcessManager class that make the code more readable. (#50026) --- .../dataframe/DataFrameAnalyticsManager.java | 10 +- .../process/AnalyticsProcessManager.java | 150 +++++++++--------- .../process/AnalyticsProcessManagerTests.java | 96 ++++++----- 3 files changed, 134 insertions(+), 122 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java index 76fc588027943..8e89113be7eba 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java @@ -233,15 +233,7 @@ private void startAnalytics(DataFrameAnalyticsTask task, DataFrameAnalyticsConfi DataFrameAnalyticsTaskState analyzingState = new DataFrameAnalyticsTaskState(DataFrameAnalyticsState.ANALYZING, task.getAllocationId(), null); task.updatePersistentTaskState(analyzingState, ActionListener.wrap( - updatedTask -> processManager.runJob(task, config, dataExtractorFactory, - error -> { - if (error != null) { - task.updateState(DataFrameAnalyticsState.FAILED, error.getMessage()); - } else { - auditor.info(config.getId(), Messages.DATA_FRAME_ANALYTICS_AUDIT_FINISHED_ANALYSIS); - task.markAsCompleted(); - } - }), + updatedTask -> processManager.runJob(task, config, dataExtractorFactory), error -> { if (ExceptionsHelper.unwrapCause(error) instanceof ResourceNotFoundException) { // Task has stopped diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java index 815d8478a5275..ce981ad17a98a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java @@ -8,6 +8,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.action.admin.indices.refresh.RefreshAction; import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; import org.elasticsearch.action.search.SearchResponse; @@ -90,19 +91,19 @@ public AnalyticsProcessManager(Client client, this.trainedModelProvider = Objects.requireNonNull(trainedModelProvider); } - public void runJob(DataFrameAnalyticsTask task, DataFrameAnalyticsConfig config, DataFrameDataExtractorFactory dataExtractorFactory, - Consumer finishHandler) { + public void runJob(DataFrameAnalyticsTask task, DataFrameAnalyticsConfig config, DataFrameDataExtractorFactory dataExtractorFactory) { executorServiceForJob.execute(() -> { - ProcessContext processContext = new ProcessContext(config.getId()); + ProcessContext processContext = new ProcessContext(config); synchronized (processContextByAllocation) { if (task.isStopping()) { // The task was requested to stop before we created the process context - finishHandler.accept(null); + auditor.info(config.getId(), Messages.DATA_FRAME_ANALYTICS_AUDIT_FINISHED_ANALYSIS); + task.markAsCompleted(); return; } if (processContextByAllocation.putIfAbsent(task.getAllocationId(), processContext) != null) { - finishHandler.accept( - ExceptionsHelper.serverError("[" + config.getId() + "] Could not create process as one already exists")); + task.updateState( + DataFrameAnalyticsState.FAILED, "[" + config.getId() + "] Could not create process as one already exists"); return; } } @@ -113,13 +114,13 @@ public void runJob(DataFrameAnalyticsTask task, DataFrameAnalyticsConfig config, // Fetch existing model state (if any) BytesReference state = getModelState(config); - if (processContext.startProcess(dataExtractorFactory, config, task, state)) { - executorServiceForProcess.execute(() -> processResults(processContext)); - executorServiceForProcess.execute(() -> processData(task, config, processContext.dataExtractor, - processContext.process, processContext.resultProcessor, finishHandler, state)); + if (processContext.startProcess(dataExtractorFactory, task, state)) { + executorServiceForProcess.execute(() -> processContext.resultProcessor.get().process(processContext.process.get())); + executorServiceForProcess.execute(() -> processData(task, processContext, state)); } else { processContextByAllocation.remove(task.getAllocationId()); - finishHandler.accept(null); + auditor.info(config.getId(), Messages.DATA_FRAME_ANALYTICS_AUDIT_FINISHED_ANALYSIS); + task.markAsCompleted(); } }); } @@ -140,26 +141,18 @@ private BytesReference getModelState(DataFrameAnalyticsConfig config) { } } - private void processResults(ProcessContext processContext) { + private void processData(DataFrameAnalyticsTask task, ProcessContext processContext, BytesReference state) { + DataFrameAnalyticsConfig config = processContext.config; + DataFrameDataExtractor dataExtractor = processContext.dataExtractor.get(); + AnalyticsProcess process = processContext.process.get(); + AnalyticsResultProcessor resultProcessor = processContext.resultProcessor.get(); try { - processContext.resultProcessor.process(processContext.process); - } catch (Exception e) { - processContext.setFailureReason(e.getMessage()); - } - } - - private void processData(DataFrameAnalyticsTask task, DataFrameAnalyticsConfig config, DataFrameDataExtractor dataExtractor, - AnalyticsProcess process, AnalyticsResultProcessor resultProcessor, - Consumer finishHandler, BytesReference state) { - - try { - ProcessContext processContext = processContextByAllocation.get(task.getAllocationId()); writeHeaderRecord(dataExtractor, process); writeDataRows(dataExtractor, process, config.getAnalysis(), task.getProgressTracker()); process.writeEndOfDataMessage(); process.flushStream(); - restoreState(config, state, process, finishHandler); + restoreState(task, config, state, process); LOGGER.info("[{}] Waiting for result processor to complete", config.getId()); resultProcessor.awaitForCompletion(); @@ -168,26 +161,34 @@ private void processData(DataFrameAnalyticsTask task, DataFrameAnalyticsConfig c refreshDest(config); LOGGER.info("[{}] Result processor has completed", config.getId()); } catch (Exception e) { - if (task.isStopping() == false) { - String errorMsg = new ParameterizedMessage("[{}] Error while processing data [{}]", config.getId(), e.getMessage()) - .getFormattedMessage(); + if (task.isStopping()) { + // Errors during task stopping are expected but we still want to log them just in case. + String errorMsg = + new ParameterizedMessage( + "[{}] Error while processing data [{}]; task is stopping", config.getId(), e.getMessage()).getFormattedMessage(); + LOGGER.debug(errorMsg, e); + } else { + String errorMsg = + new ParameterizedMessage("[{}] Error while processing data [{}]", config.getId(), e.getMessage()).getFormattedMessage(); LOGGER.error(errorMsg, e); - processContextByAllocation.get(task.getAllocationId()).setFailureReason(errorMsg); + processContext.setFailureReason(errorMsg); } } finally { closeProcess(task); - ProcessContext processContext = processContextByAllocation.remove(task.getAllocationId()); + processContextByAllocation.remove(task.getAllocationId()); LOGGER.debug("Removed process context for task [{}]; [{}] processes still running", config.getId(), processContextByAllocation.size()); if (processContext.getFailureReason() == null) { // This results in marking the persistent task as complete LOGGER.info("[{}] Marking task completed", config.getId()); - finishHandler.accept(null); + auditor.info(config.getId(), Messages.DATA_FRAME_ANALYTICS_AUDIT_FINISHED_ANALYSIS); + task.markAsCompleted(); } else { LOGGER.error("[{}] Marking task failed; {}", config.getId(), processContext.getFailureReason()); task.updateState(DataFrameAnalyticsState.FAILED, processContext.getFailureReason()); + // Note: We are not marking the task as failed here as we want the user to be able to inspect the failure reason. } } } @@ -239,8 +240,8 @@ private void writeHeaderRecord(DataFrameDataExtractor dataExtractor, AnalyticsPr process.writeRecord(headerRecord); } - private void restoreState(DataFrameAnalyticsConfig config, @Nullable BytesReference state, AnalyticsProcess process, - Consumer failureHandler) { + private void restoreState(DataFrameAnalyticsTask task, DataFrameAnalyticsConfig config, @Nullable BytesReference state, + AnalyticsProcess process) { if (config.getAnalysis().persistsState() == false) { LOGGER.debug("[{}] Analysis does not support state", config.getId()); return; @@ -258,7 +259,7 @@ private void restoreState(DataFrameAnalyticsConfig config, @Nullable BytesRefere process.restoreState(state); } catch (Exception e) { LOGGER.error(new ParameterizedMessage("[{}] Failed to restore state", process.getConfig().jobId()), e); - failureHandler.accept(ExceptionsHelper.serverError("Failed to restore state", e)); + task.updateState(DataFrameAnalyticsState.FAILED, "Failed to restore state: " + e.getMessage()); } } @@ -293,9 +294,10 @@ private void closeProcess(DataFrameAnalyticsTask task) { ProcessContext processContext = processContextByAllocation.get(task.getAllocationId()); try { - processContext.process.close(); + processContext.process.get().close(); LOGGER.info("[{}] Closed process", configId); } catch (Exception e) { + LOGGER.error("[" + configId + "] Error closing data frame analyzer process", e); String errorMsg = new ParameterizedMessage( "[{}] Error closing data frame analyzer process [{}]", configId, e.getMessage()).getFormattedMessage(); processContext.setFailureReason(errorMsg); @@ -323,42 +325,41 @@ int getProcessContextCount() { class ProcessContext { - private final String id; - private volatile AnalyticsProcess process; - private volatile DataFrameDataExtractor dataExtractor; - private volatile AnalyticsResultProcessor resultProcessor; - private volatile boolean processKilled; - private volatile String failureReason; + private final DataFrameAnalyticsConfig config; + private final SetOnce> process = new SetOnce<>(); + private final SetOnce dataExtractor = new SetOnce<>(); + private final SetOnce resultProcessor = new SetOnce<>(); + private final SetOnce failureReason = new SetOnce<>(); - ProcessContext(String id) { - this.id = Objects.requireNonNull(id); + ProcessContext(DataFrameAnalyticsConfig config) { + this.config = Objects.requireNonNull(config); } - synchronized String getFailureReason() { - return failureReason; + String getFailureReason() { + return failureReason.get(); } - synchronized void setFailureReason(String failureReason) { - // Only set the new reason if there isn't one already as we want to keep the first reason - if (this.failureReason == null && failureReason != null) { - this.failureReason = failureReason; + void setFailureReason(String failureReason) { + if (failureReason == null) { + return; } + // Only set the new reason if there isn't one already as we want to keep the first reason (most likely the root cause). + this.failureReason.trySet(failureReason); } synchronized void stop() { - LOGGER.debug("[{}] Stopping process", id); - processKilled = true; - if (dataExtractor != null) { - dataExtractor.cancel(); + LOGGER.debug("[{}] Stopping process", config.getId()); + if (dataExtractor.get() != null) { + dataExtractor.get().cancel(); } - if (resultProcessor != null) { - resultProcessor.cancel(); + if (resultProcessor.get() != null) { + resultProcessor.get().cancel(); } - if (process != null) { + if (process.get() != null) { try { - process.kill(); + process.get().kill(); } catch (IOException e) { - LOGGER.error(new ParameterizedMessage("[{}] Failed to kill process", id), e); + LOGGER.error(new ParameterizedMessage("[{}] Failed to kill process", config.getId()), e); } } } @@ -366,16 +367,17 @@ synchronized void stop() { /** * @return {@code true} if the process was started or {@code false} if it was not because it was stopped in the meantime */ - synchronized boolean startProcess(DataFrameDataExtractorFactory dataExtractorFactory, DataFrameAnalyticsConfig config, - DataFrameAnalyticsTask task, @Nullable BytesReference state) { - if (processKilled) { + synchronized boolean startProcess(DataFrameDataExtractorFactory dataExtractorFactory, + DataFrameAnalyticsTask task, + @Nullable BytesReference state) { + if (task.isStopping()) { // The job was stopped before we started the process so no need to start it return false; } - dataExtractor = dataExtractorFactory.newExtractor(false); + dataExtractor.set(dataExtractorFactory.newExtractor(false)); AnalyticsProcessConfig analyticsProcessConfig = - createProcessConfig(config, dataExtractor, dataExtractorFactory.getExtractedFields()); + createProcessConfig(dataExtractor.get(), dataExtractorFactory.getExtractedFields()); LOGGER.trace("[{}] creating analytics process with config [{}]", config.getId(), Strings.toString(analyticsProcessConfig)); // If we have no rows, that means there is no data so no point in starting the native process // just finish the task @@ -383,19 +385,16 @@ synchronized boolean startProcess(DataFrameDataExtractorFactory dataExtractorFac LOGGER.info("[{}] no data found to analyze. Will not start analytics native process.", config.getId()); return false; } - process = createProcess(task, config, analyticsProcessConfig, state); - DataFrameRowsJoiner dataFrameRowsJoiner = new DataFrameRowsJoiner(config.getId(), client, - dataExtractorFactory.newExtractor(true)); - resultProcessor = new AnalyticsResultProcessor( - config, dataFrameRowsJoiner, task.getProgressTracker(), trainedModelProvider, auditor, dataExtractor.getFieldNames()); + process.set(createProcess(task, config, analyticsProcessConfig, state)); + resultProcessor.set(createResultProcessor(task, dataExtractorFactory)); return true; } - private AnalyticsProcessConfig createProcessConfig( - DataFrameAnalyticsConfig config, DataFrameDataExtractor dataExtractor, ExtractedFields extractedFields) { + private AnalyticsProcessConfig createProcessConfig(DataFrameDataExtractor dataExtractor, + ExtractedFields extractedFields) { DataFrameDataExtractor.DataSummary dataSummary = dataExtractor.collectDataSummary(); Set categoricalFields = dataExtractor.getCategoricalFields(config.getAnalysis()); - AnalyticsProcessConfig processConfig = new AnalyticsProcessConfig( + return new AnalyticsProcessConfig( config.getId(), dataSummary.rows, dataSummary.cols, @@ -405,7 +404,14 @@ private AnalyticsProcessConfig createProcessConfig( categoricalFields, config.getAnalysis(), extractedFields); - return processConfig; + } + + private AnalyticsResultProcessor createResultProcessor(DataFrameAnalyticsTask task, + DataFrameDataExtractorFactory dataExtractorFactory) { + DataFrameRowsJoiner dataFrameRowsJoiner = + new DataFrameRowsJoiner(config.getId(), client, dataExtractorFactory.newExtractor(true)); + return new AnalyticsResultProcessor( + config, dataFrameRowsJoiner, task.getProgressTracker(), trainedModelProvider, auditor, dataExtractor.get().getFieldNames()); } } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManagerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManagerTests.java index 4a0d5fa7f36df..915d6c29efb4d 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManagerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManagerTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfigTests; +import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsState; import org.elasticsearch.xpack.ml.dataframe.DataFrameAnalyticsTask; import org.elasticsearch.xpack.ml.dataframe.extractor.DataFrameDataExtractor; import org.elasticsearch.xpack.ml.dataframe.extractor.DataFrameDataExtractorFactory; @@ -22,12 +23,10 @@ import org.elasticsearch.xpack.ml.inference.persistence.TrainedModelProvider; import org.elasticsearch.xpack.ml.notifications.DataFrameAnalyticsAuditor; import org.junit.Before; -import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import java.util.List; import java.util.concurrent.ExecutorService; -import java.util.function.Consumer; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -37,7 +36,6 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -66,8 +64,6 @@ public class AnalyticsProcessManagerTests extends ESTestCase { private DataFrameAnalyticsConfig dataFrameAnalyticsConfig; private DataFrameDataExtractorFactory dataExtractorFactory; private DataFrameDataExtractor dataExtractor; - private Consumer finishHandler; - private ArgumentCaptor exceptionCaptor; private AnalyticsProcessManager processManager; @SuppressWarnings("unchecked") @@ -97,9 +93,6 @@ public void setUpMocks() { dataExtractorFactory = mock(DataFrameDataExtractorFactory.class); when(dataExtractorFactory.newExtractor(anyBoolean())).thenReturn(dataExtractor); when(dataExtractorFactory.getExtractedFields()).thenReturn(mock(ExtractedFields.class)); - finishHandler = mock(Consumer.class); - - exceptionCaptor = ArgumentCaptor.forClass(Exception.class); processManager = new AnalyticsProcessManager( client, executorServiceForJob, executorServiceForProcess, processFactory, auditor, trainedModelProvider); @@ -108,54 +101,68 @@ public void setUpMocks() { public void testRunJob_TaskIsStopping() { when(task.isStopping()).thenReturn(true); - processManager.runJob(task, dataFrameAnalyticsConfig, dataExtractorFactory, finishHandler); + processManager.runJob(task, dataFrameAnalyticsConfig, dataExtractorFactory); assertThat(processManager.getProcessContextCount(), equalTo(0)); - verify(finishHandler).accept(null); - verifyNoMoreInteractions(finishHandler); + InOrder inOrder = inOrder(task); + inOrder.verify(task).isStopping(); + inOrder.verify(task).markAsCompleted(); + verifyNoMoreInteractions(task); } public void testRunJob_ProcessContextAlreadyExists() { - processManager.runJob(task, dataFrameAnalyticsConfig, dataExtractorFactory, finishHandler); + processManager.runJob(task, dataFrameAnalyticsConfig, dataExtractorFactory); assertThat(processManager.getProcessContextCount(), equalTo(1)); - processManager.runJob(task, dataFrameAnalyticsConfig, dataExtractorFactory, finishHandler); + processManager.runJob(task, dataFrameAnalyticsConfig, dataExtractorFactory); assertThat(processManager.getProcessContextCount(), equalTo(1)); - verify(finishHandler).accept(exceptionCaptor.capture()); - verifyNoMoreInteractions(finishHandler); - - Exception e = exceptionCaptor.getValue(); - assertThat(e.getMessage(), equalTo("[config-id] Could not create process as one already exists")); + InOrder inOrder = inOrder(task); + inOrder.verify(task).isStopping(); + inOrder.verify(task).getAllocationId(); + inOrder.verify(task).isStopping(); + inOrder.verify(task).getProgressTracker(); + inOrder.verify(task).isStopping(); + inOrder.verify(task).getAllocationId(); + inOrder.verify(task).updateState(DataFrameAnalyticsState.FAILED, "[config-id] Could not create process as one already exists"); + verifyNoMoreInteractions(task); } public void testRunJob_EmptyDataFrame() { when(dataExtractor.collectDataSummary()).thenReturn(new DataFrameDataExtractor.DataSummary(0, NUM_COLS)); - processManager.runJob(task, dataFrameAnalyticsConfig, dataExtractorFactory, finishHandler); + processManager.runJob(task, dataFrameAnalyticsConfig, dataExtractorFactory); assertThat(processManager.getProcessContextCount(), equalTo(0)); // Make sure the process context did not leak - InOrder inOrder = inOrder(dataExtractor, executorServiceForProcess, process, finishHandler); + InOrder inOrder = inOrder(dataExtractor, executorServiceForProcess, process, task); + inOrder.verify(task).isStopping(); + inOrder.verify(task).getAllocationId(); + inOrder.verify(task).isStopping(); inOrder.verify(dataExtractor).collectDataSummary(); inOrder.verify(dataExtractor).getCategoricalFields(dataFrameAnalyticsConfig.getAnalysis()); - inOrder.verify(finishHandler).accept(null); - verifyNoMoreInteractions(dataExtractor, executorServiceForProcess, process, finishHandler); + inOrder.verify(task).getAllocationId(); + inOrder.verify(task).markAsCompleted(); + verifyNoMoreInteractions(dataExtractor, executorServiceForProcess, process, task); } public void testRunJob_Ok() { - processManager.runJob(task, dataFrameAnalyticsConfig, dataExtractorFactory, finishHandler); + processManager.runJob(task, dataFrameAnalyticsConfig, dataExtractorFactory); assertThat(processManager.getProcessContextCount(), equalTo(1)); - InOrder inOrder = inOrder(dataExtractor, executorServiceForProcess, process, finishHandler); + InOrder inOrder = inOrder(dataExtractor, executorServiceForProcess, process, task); + inOrder.verify(task).isStopping(); + inOrder.verify(task).getAllocationId(); + inOrder.verify(task).isStopping(); inOrder.verify(dataExtractor).collectDataSummary(); inOrder.verify(dataExtractor).getCategoricalFields(dataFrameAnalyticsConfig.getAnalysis()); inOrder.verify(process).isProcessAlive(); + inOrder.verify(task).getProgressTracker(); inOrder.verify(dataExtractor).getFieldNames(); inOrder.verify(executorServiceForProcess, times(2)).execute(any()); // 'processData' and 'processResults' threads - verifyNoMoreInteractions(dataExtractor, executorServiceForProcess, process, finishHandler); + verifyNoMoreInteractions(dataExtractor, executorServiceForProcess, process, task); } public void testProcessContext_GetSetFailureReason() { - AnalyticsProcessManager.ProcessContext processContext = processManager.new ProcessContext(CONFIG_ID); + AnalyticsProcessManager.ProcessContext processContext = processManager.new ProcessContext(dataFrameAnalyticsConfig); assertThat(processContext.getFailureReason(), is(nullValue())); processContext.setFailureReason("reason1"); @@ -167,50 +174,57 @@ public void testProcessContext_GetSetFailureReason() { processContext.setFailureReason("reason2"); assertThat(processContext.getFailureReason(), equalTo("reason1")); - verifyNoMoreInteractions(dataExtractor, process, finishHandler); + verifyNoMoreInteractions(dataExtractor, process, task); } - public void testProcessContext_StartProcess_ProcessAlreadyKilled() { - AnalyticsProcessManager.ProcessContext processContext = processManager.new ProcessContext(CONFIG_ID); + public void testProcessContext_StartProcess_TaskAlreadyStopped() { + when(task.isStopping()).thenReturn(true); + + AnalyticsProcessManager.ProcessContext processContext = processManager.new ProcessContext(dataFrameAnalyticsConfig); processContext.stop(); - assertThat(processContext.startProcess(dataExtractorFactory, dataFrameAnalyticsConfig, task, null), is(false)); + assertThat(processContext.startProcess(dataExtractorFactory, task, null), is(false)); - verifyNoMoreInteractions(dataExtractor, process, finishHandler); + InOrder inOrder = inOrder(dataExtractor, process, task); + inOrder.verify(task).isStopping(); + verifyNoMoreInteractions(dataExtractor, process, task); } public void testProcessContext_StartProcess_EmptyDataFrame() { when(dataExtractor.collectDataSummary()).thenReturn(new DataFrameDataExtractor.DataSummary(0, NUM_COLS)); - AnalyticsProcessManager.ProcessContext processContext = processManager.new ProcessContext(CONFIG_ID); - assertThat(processContext.startProcess(dataExtractorFactory, dataFrameAnalyticsConfig, task, null), is(false)); + AnalyticsProcessManager.ProcessContext processContext = processManager.new ProcessContext(dataFrameAnalyticsConfig); + assertThat(processContext.startProcess(dataExtractorFactory, task, null), is(false)); - InOrder inOrder = inOrder(dataExtractor, process, finishHandler); + InOrder inOrder = inOrder(dataExtractor, process, task); + inOrder.verify(task).isStopping(); inOrder.verify(dataExtractor).collectDataSummary(); inOrder.verify(dataExtractor).getCategoricalFields(dataFrameAnalyticsConfig.getAnalysis()); - verifyNoMoreInteractions(dataExtractor, process, finishHandler); + verifyNoMoreInteractions(dataExtractor, process, task); } public void testProcessContext_StartAndStop() throws Exception { - AnalyticsProcessManager.ProcessContext processContext = processManager.new ProcessContext(CONFIG_ID); - assertThat(processContext.startProcess(dataExtractorFactory, dataFrameAnalyticsConfig, task, null), is(true)); + AnalyticsProcessManager.ProcessContext processContext = processManager.new ProcessContext(dataFrameAnalyticsConfig); + assertThat(processContext.startProcess(dataExtractorFactory, task, null), is(true)); processContext.stop(); - InOrder inOrder = inOrder(dataExtractor, process, finishHandler); + InOrder inOrder = inOrder(dataExtractor, process, task); // startProcess + inOrder.verify(task).isStopping(); inOrder.verify(dataExtractor).collectDataSummary(); inOrder.verify(dataExtractor).getCategoricalFields(dataFrameAnalyticsConfig.getAnalysis()); inOrder.verify(process).isProcessAlive(); + inOrder.verify(task).getProgressTracker(); inOrder.verify(dataExtractor).getFieldNames(); // stop inOrder.verify(dataExtractor).cancel(); inOrder.verify(process).kill(); - verifyNoMoreInteractions(dataExtractor, process, finishHandler); + verifyNoMoreInteractions(dataExtractor, process, task); } public void testProcessContext_Stop() { - AnalyticsProcessManager.ProcessContext processContext = processManager.new ProcessContext(CONFIG_ID); + AnalyticsProcessManager.ProcessContext processContext = processManager.new ProcessContext(dataFrameAnalyticsConfig); processContext.stop(); - verifyNoMoreInteractions(dataExtractor, process, finishHandler); + verifyNoMoreInteractions(dataExtractor, process, task); } } From 813ed0c67778e4762173f5e898e22fcd94b76073 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 11 Dec 2019 10:00:41 +0200 Subject: [PATCH 150/686] [ml] Fix randomize_seed versions and unmute BWC tests (#50027) ... now that #49990 has been backported. Relates #49990 --- .../xpack/core/ml/dataframe/analyses/Classification.java | 6 +++--- .../xpack/core/ml/dataframe/analyses/Regression.java | 6 +++--- .../test/mixed_cluster/90_ml_data_frame_analytics_crud.yml | 6 +----- .../test/old_cluster/90_ml_data_frame_analytics_crud.yml | 3 --- .../upgraded_cluster/90_ml_data_frame_analytics_crud.yml | 6 +----- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java index cd96b815fc11e..ed4cb1fe18f8e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java @@ -119,7 +119,7 @@ public Classification(StreamInput in) throws IOException { predictionFieldName = in.readOptionalString(); numTopClasses = in.readOptionalVInt(); trainingPercent = in.readDouble(); - if (in.getVersion().onOrAfter(Version.CURRENT)) { + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { randomizeSeed = in.readOptionalLong(); } else { randomizeSeed = Randomness.get().nextLong(); @@ -163,7 +163,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(predictionFieldName); out.writeOptionalVInt(numTopClasses); out.writeDouble(trainingPercent); - if (out.getVersion().onOrAfter(Version.CURRENT)) { + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { out.writeOptionalLong(randomizeSeed); } } @@ -180,7 +180,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(PREDICTION_FIELD_NAME.getPreferredName(), predictionFieldName); } builder.field(TRAINING_PERCENT.getPreferredName(), trainingPercent); - if (version.onOrAfter(Version.CURRENT)) { + if (version.onOrAfter(Version.V_7_6_0)) { builder.field(RANDOMIZE_SEED.getPreferredName(), randomizeSeed); } builder.endObject(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java index dd8f6a91272c2..8fffcd0f573da 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java @@ -91,7 +91,7 @@ public Regression(StreamInput in) throws IOException { boostedTreeParams = new BoostedTreeParams(in); predictionFieldName = in.readOptionalString(); trainingPercent = in.readDouble(); - if (in.getVersion().onOrAfter(Version.CURRENT)) { + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { randomizeSeed = in.readOptionalLong(); } else { randomizeSeed = Randomness.get().nextLong(); @@ -130,7 +130,7 @@ public void writeTo(StreamOutput out) throws IOException { boostedTreeParams.writeTo(out); out.writeOptionalString(predictionFieldName); out.writeDouble(trainingPercent); - if (out.getVersion().onOrAfter(Version.CURRENT)) { + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { out.writeOptionalLong(randomizeSeed); } } @@ -146,7 +146,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(PREDICTION_FIELD_NAME.getPreferredName(), predictionFieldName); } builder.field(TRAINING_PERCENT.getPreferredName(), trainingPercent); - if (version.onOrAfter(Version.CURRENT)) { + if (version.onOrAfter(Version.V_7_6_0)) { builder.field(RANDOMIZE_SEED.getPreferredName(), randomizeSeed); } builder.endObject(); diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml index 8082147160718..7780691b2bbbd 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,8 +1,3 @@ -setup: - - skip: - version: "all" - reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" - --- "Get old outlier_detection job": @@ -65,6 +60,7 @@ setup: - match: { data_frame_analytics.0.dest.index: "old_cluster_regression_job_results" } - match: { data_frame_analytics.0.analysis.regression.dependent_variable: "foo" } - match: { data_frame_analytics.0.analysis.regression.training_percent: 100.0 } + - is_true: data_frame_analytics.0.analysis.regression.randomize_seed --- "Get old regression job stats": diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml index ba2cf40411672..fe160bba15f23 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,7 +1,4 @@ setup: - - skip: - version: "all" - reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" - do: index: diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml index 462a1fd76c011..14438883f0da1 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,8 +1,3 @@ -setup: - - skip: - version: "all" - reason: "Until backport of https://github.com/elastic/elasticsearch/issues/49690" - --- "Get old cluster outlier_detection job": @@ -45,6 +40,7 @@ setup: - match: { data_frame_analytics.0.dest.index: "old_cluster_regression_job_results" } - match: { data_frame_analytics.0.analysis.regression.dependent_variable: "foo" } - match: { data_frame_analytics.0.analysis.regression.training_percent: 100.0 } + - is_true: data_frame_analytics.0.analysis.regression.randomize_seed --- "Get old cluster regression job stats": From 1dd2584a9eb817e4a6df772c5d6af11cde69113d Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 11 Dec 2019 15:14:33 +0200 Subject: [PATCH 151/686] [ML] Avoid classification integ test training on single class (#50072) The `ClassificationIT.testTwoJobsWithSameRandomizeSeedUseSameTrainingSet` test was previously set up to just have 10 rows. With `training_percent` of 50%, only 5 rows will be used for training. There is a good chance that all 5 rows will be of one class which results to failure. This commit increases the rows to 100. Now 50 rows should be used for training and the chance of failure should be very small. --- .../elasticsearch/xpack/ml/integration/ClassificationIT.java | 5 ++++- .../integration/MlNativeDataFrameAnalyticsIntegTestCase.java | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java index e7c0ccd0e0554..0e49043fcfbe5 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java @@ -274,7 +274,10 @@ public void testDependentVariableCardinalityTooHighButWithQueryMakesItWithinRang public void testTwoJobsWithSameRandomizeSeedUseSameTrainingSet() throws Exception { String sourceIndex = "classification_two_jobs_with_same_randomize_seed_source"; String dependentVariable = KEYWORD_FIELD; - indexData(sourceIndex, 10, 0, dependentVariable); + + // We use 100 rows as we can't set this too low. If too low it is possible + // we only train with rows of one of the two classes which leads to a failure. + indexData(sourceIndex, 100, 0, dependentVariable); String firstJobId = "classification_two_jobs_with_same_randomize_seed_1"; String firstJobDestIndex = firstJobId + "_dest"; diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java index 99223247d7305..8ff82c28b36e0 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java @@ -259,7 +259,7 @@ private static List fetchAllAuditMessages(String dataFrameAnalyticsId) { protected static Set getTrainingRowsIds(String index) { Set trainingRowsIds = new HashSet<>(); - SearchResponse hits = client().prepareSearch(index).get(); + SearchResponse hits = client().prepareSearch(index).setSize(10000).get(); for (SearchHit hit : hits.getHits()) { Map sourceAsMap = hit.getSourceAsMap(); assertThat(sourceAsMap.containsKey("ml"), is(true)); From 3da8cf03484e9993df4aeb94b2d8cfaf1a8b02cb Mon Sep 17 00:00:00 2001 From: Przemko Robakowski Date: Wed, 11 Dec 2019 14:52:04 +0100 Subject: [PATCH 152/686] CSV ingest processor (#49509) * CSV Processor for Ingest This change adds new ingest processor that breaks line from CSV file into separate fields. By default it conforms to RFC 4180 but can be tweaked. Closes #49113 --- docs/reference/ingest/ingest-node.asciidoc | 1 + docs/reference/ingest/processors/csv.asciidoc | 33 +++ .../ingest/common/CsvParser.java | 206 ++++++++++++++++ .../ingest/common/CsvProcessor.java | 108 +++++++++ .../ingest/common/IngestCommonPlugin.java | 3 +- .../ingest/common/CsvProcessorTests.java | 221 ++++++++++++++++++ .../rest-api-spec/test/ingest/250_csv.yml | 164 +++++++++++++ 7 files changed, 735 insertions(+), 1 deletion(-) create mode 100644 docs/reference/ingest/processors/csv.asciidoc create mode 100644 modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/CsvParser.java create mode 100644 modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/CsvProcessor.java create mode 100644 modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorTests.java create mode 100644 modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/250_csv.yml diff --git a/docs/reference/ingest/ingest-node.asciidoc b/docs/reference/ingest/ingest-node.asciidoc index 0da0fd19e16ef..596bda67d3ed8 100644 --- a/docs/reference/ingest/ingest-node.asciidoc +++ b/docs/reference/ingest/ingest-node.asciidoc @@ -825,6 +825,7 @@ include::processors/append.asciidoc[] include::processors/bytes.asciidoc[] include::processors/circle.asciidoc[] include::processors/convert.asciidoc[] +include::processors/csv.asciidoc[] include::processors/date.asciidoc[] include::processors/date-index-name.asciidoc[] include::processors/dissect.asciidoc[] diff --git a/docs/reference/ingest/processors/csv.asciidoc b/docs/reference/ingest/processors/csv.asciidoc new file mode 100644 index 0000000000000..c589c9eb4361c --- /dev/null +++ b/docs/reference/ingest/processors/csv.asciidoc @@ -0,0 +1,33 @@ +[[csv-processor]] +=== CSV Processor +Extracts fields from CSV line out of a single text field within a document. Any empty field in CSV will be skipped. + +[[csv-options]] +.CSV Options +[options="header"] +|====== +| Name | Required | Default | Description +| `field` | yes | - | The field to extract data from +| `target_fields` | yes | - | The array of fields to assign extracted values to +| `separator` | no | , | Separator used in CSV, has to be single character string +| `quote` | no | " | Quote used in CSV, has to be single character string +| `ignore_missing` | no | `true` | If `true` and `field` does not exist, the processor quietly exits without modifying the document +| `trim` | no | `false` | Trim whitespaces in unquoted fields +include::common-options.asciidoc[] +|====== + +[source,js] +-------------------------------------------------- +{ + "csv": { + "field": "my_field", + "target_fields": ["field1, field2"], + } +} +-------------------------------------------------- +// NOTCONSOLE + +If the `trim` option is enabled then any whitespace in the beginning and in the end of each unquoted field will be trimmed. +For example with configuration above, a value of `A, B` will result in field `field2` +having value `{nbsp}B` (with space at the beginning). If `trim` is enabled `A, B` will result in field `field2` +having value `B` (no whitespace). Quoted fields will be left untouched. diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/CsvParser.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/CsvParser.java new file mode 100644 index 0000000000000..077d12684e9a1 --- /dev/null +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/CsvParser.java @@ -0,0 +1,206 @@ +/* + * 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. + */ + +package org.elasticsearch.ingest.common; + +import org.elasticsearch.ingest.IngestDocument; + +final class CsvParser { + + private static final char LF = '\n'; + private static final char CR = '\r'; + private static final char SPACE = ' '; + private static final char TAB = '\t'; + + private enum State { + START, UNQUOTED, QUOTED, QUOTED_END + } + + private final char quote; + private final char separator; + private final boolean trim; + private final String[] headers; + private final IngestDocument ingestDocument; + private final StringBuilder builder = new StringBuilder(); + private State state = State.START; + private String line; + private int currentHeader = 0; + private int startIndex = 0; + private int length; + private int currentIndex; + + CsvParser(IngestDocument ingestDocument, char quote, char separator, boolean trim, String[] headers) { + this.ingestDocument = ingestDocument; + this.quote = quote; + this.separator = separator; + this.trim = trim; + this.headers = headers; + } + + void process(String line) { + this.line = line; + length = line.length(); + for (currentIndex = 0; currentIndex < length; currentIndex++) { + switch (state) { + case START: + if (processStart()) { + return; + } + break; + case UNQUOTED: + if (processUnquoted()) { + return; + } + break; + case QUOTED: + processQuoted(); + break; + case QUOTED_END: + if (processQuotedEnd()) { + return; + } + break; + } + } + + //we've reached end of string, we need to handle last field + switch (state) { + case UNQUOTED: + setField(length); + break; + case QUOTED_END: + setField(length - 1); + break; + case QUOTED: + throw new IllegalArgumentException("Unmatched quote"); + } + } + + private boolean processStart() { + for (; currentIndex < length; currentIndex++) { + char c = currentChar(); + if (c == quote) { + state = State.QUOTED; + builder.setLength(0); + startIndex = currentIndex + 1; + return false; + } else if (c == separator) { + startIndex++; + if (nextHeader()) { + return true; + } + } else if (isWhitespace(c)) { + if (trim) { + startIndex++; + } + } else { + state = State.UNQUOTED; + builder.setLength(0); + return false; + } + } + return true; + } + + private boolean processUnquoted() { + int spaceCount = 0; + for (; currentIndex < length; currentIndex++) { + char c = currentChar(); + if (c == LF || c == CR || c == quote) { + throw new IllegalArgumentException("Illegal character inside unquoted field at " + currentIndex); + } else if (trim && isWhitespace(c)) { + spaceCount++; + } else if (c == separator) { + state = State.START; + if (setField(currentIndex - spaceCount)) { + return true; + } + startIndex = currentIndex + 1; + return false; + } else { + spaceCount = 0; + } + } + return false; + } + + private void processQuoted() { + for (; currentIndex < length; currentIndex++) { + if (currentChar() == quote) { + state = State.QUOTED_END; + break; + } + } + } + + private boolean processQuotedEnd() { + char c = currentChar(); + if (c == quote) { + builder.append(line, startIndex, currentIndex - 1).append(quote); + startIndex = currentIndex + 1; + state = State.QUOTED; + return false; + } + boolean shouldSetField = true; + for (; currentIndex < length; currentIndex++) { + c = currentChar(); + if (isWhitespace(c)) { + if (shouldSetField) { + if (setField(currentIndex - 1)) { + return true; + } + shouldSetField = false; + } + } else if (c == separator) { + if (shouldSetField && setField(currentIndex - 1)) { + return true; + } + startIndex = currentIndex + 1; + state = State.START; + return false; + } else { + throw new IllegalArgumentException("character '" + c + "' after quoted field at " + currentIndex); + } + } + return true; + } + + private char currentChar() { + return line.charAt(currentIndex); + } + + private boolean isWhitespace(char c) { + return c == SPACE || c == TAB; + } + + private boolean setField(int endIndex) { + if (builder.length() == 0) { + ingestDocument.setFieldValue(headers[currentHeader], line.substring(startIndex, endIndex)); + } else { + builder.append(line, startIndex, endIndex); + ingestDocument.setFieldValue(headers[currentHeader], builder.toString()); + } + return nextHeader(); + } + + private boolean nextHeader() { + currentHeader++; + return currentHeader == headers.length; + } +} diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/CsvProcessor.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/CsvProcessor.java new file mode 100644 index 0000000000000..66d10cc239e46 --- /dev/null +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/CsvProcessor.java @@ -0,0 +1,108 @@ +/* + * 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. + */ + +package org.elasticsearch.ingest.common; + +import org.elasticsearch.ingest.AbstractProcessor; +import org.elasticsearch.ingest.ConfigurationUtils; +import org.elasticsearch.ingest.IngestDocument; + +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.ingest.ConfigurationUtils.newConfigurationException; + +/** + * A processor that breaks line from CSV file into separate fields. + * If there's more fields requested than there is in the CSV, extra field will not be present in the document after processing. + * In the same way this processor will skip any field that is empty in CSV. + * + * By default it uses rules according to RCF 4180 with one exception: whitespaces are + * allowed before or after quoted field. Processor can be tweaked with following parameters: + * + * quote: set custom quote character (defaults to ") + * separator: set custom separator (defaults to ,) + * trim: trim leading and trailing whitespaces in unquoted fields + */ +public final class CsvProcessor extends AbstractProcessor { + + public static final String TYPE = "csv"; + + private final String field; + private final String[] headers; + private final boolean trim; + private final char quote; + private final char separator; + private final boolean ignoreMissing; + + CsvProcessor(String tag, String field, String[] headers, boolean trim, char separator, char quote, boolean ignoreMissing) { + super(tag); + this.field = field; + this.headers = headers; + this.trim = trim; + this.quote = quote; + this.separator = separator; + this.ignoreMissing = ignoreMissing; + } + + @Override + public IngestDocument execute(IngestDocument ingestDocument) { + if (headers.length == 0) { + return ingestDocument; + } + + String line = ingestDocument.getFieldValue(field, String.class, ignoreMissing); + if (line == null && ignoreMissing == false) { + return ingestDocument; + } else if (line == null) { + throw new IllegalArgumentException("field [" + field + "] is null, cannot process it."); + } + new CsvParser(ingestDocument, quote, separator, trim, headers).process(line); + return ingestDocument; + } + + @Override + public String getType() { + return TYPE; + } + + public static final class Factory implements org.elasticsearch.ingest.Processor.Factory { + @Override + public CsvProcessor create(Map registry, String processorTag, + Map config) { + String field = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "field"); + String quote = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "quote", "\""); + if (quote.length() != 1) { + throw newConfigurationException(TYPE, processorTag, "quote", "quote has to be single character like \" or '"); + } + String separator = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "separator", ","); + if (separator.length() != 1) { + throw newConfigurationException(TYPE, processorTag, "separator", "separator has to be single character like , or ;"); + } + boolean trim = ConfigurationUtils.readBooleanProperty(TYPE, processorTag, config, "trim", false); + boolean ignoreMissing = ConfigurationUtils.readBooleanProperty(TYPE, processorTag, config, "ignore_missing", false); + List targetFields = ConfigurationUtils.readList(TYPE, processorTag, config, "target_fields"); + if (targetFields.isEmpty()) { + throw newConfigurationException(TYPE, processorTag, "target_fields", "target fields list can't be empty"); + } + return new CsvProcessor(processorTag, field, targetFields.toArray(String[]::new), trim, separator.charAt(0), quote.charAt(0), + ignoreMissing); + } + } +} diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java index 4f99c850e5bd3..b37e5d13e4602 100644 --- a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java @@ -88,7 +88,8 @@ public Map getProcessors(Processor.Parameters paramet entry(PipelineProcessor.TYPE, new PipelineProcessor.Factory(parameters.ingestService)), entry(DissectProcessor.TYPE, new DissectProcessor.Factory()), entry(DropProcessor.TYPE, new DropProcessor.Factory()), - entry(HtmlStripProcessor.TYPE, new HtmlStripProcessor.Factory())); + entry(HtmlStripProcessor.TYPE, new HtmlStripProcessor.Factory()), + entry(CsvProcessor.TYPE, new CsvProcessor.Factory())); } @Override diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorTests.java new file mode 100644 index 0000000000000..87da73cce129d --- /dev/null +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorTests.java @@ -0,0 +1,221 @@ +/* + * 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. + */ + +package org.elasticsearch.ingest.common; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.ingest.RandomDocumentPicks; +import org.elasticsearch.test.ESTestCase; +import org.junit.Before; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class CsvProcessorTests extends ESTestCase { + + private static final Character[] SEPARATORS = new Character[]{',', ';', '|', '.'}; + private final String quote; + private char separator; + + + public CsvProcessorTests(@Name("quote") String quote) { + this.quote = quote; + } + + @ParametersFactory + public static Iterable parameters() { + return Arrays.asList(new Object[]{"'"}, new Object[]{"\""}, new Object[]{""}); + } + + @Before + public void setup() { + separator = randomFrom(SEPARATORS); + } + + public void testExactNumberOfFields() throws Exception { + int numItems = randomIntBetween(2, 10); + Map items = new LinkedHashMap<>(); + for (int i = 0; i < numItems; i++) { + items.put(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(5, 10)); + } + String[] headers = items.keySet().toArray(new String[numItems]); + String csv = items.values().stream().map(v -> quote + v + quote).collect(Collectors.joining(separator + "")); + + IngestDocument ingestDocument = processDocument(headers, csv); + + items.forEach((key, value) -> assertEquals(value, ingestDocument.getFieldValue(key, String.class))); + } + + public void testLessFieldsThanHeaders() throws Exception { + int numItems = randomIntBetween(4, 10); + Map items = new LinkedHashMap<>(); + for (int i = 0; i < numItems; i++) { + items.put(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(5, 10)); + } + String[] headers = items.keySet().toArray(new String[numItems]); + String csv = items.values().stream().map(v -> quote + v + quote).limit(3).collect(Collectors.joining(separator + "")); + + IngestDocument ingestDocument = processDocument(headers, csv); + + items.keySet().stream().skip(3).forEach(key -> assertFalse(ingestDocument.hasField(key))); + items.entrySet().stream().limit(3).forEach(e -> assertEquals(e.getValue(), ingestDocument.getFieldValue(e.getKey(), String.class))); + } + + public void testLessHeadersThanFields() throws Exception { + int numItems = randomIntBetween(5, 10); + Map items = new LinkedHashMap<>(); + for (int i = 0; i < numItems; i++) { + items.put(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(5, 10)); + } + String[] headers = items.keySet().stream().limit(3).toArray(String[]::new); + String csv = items.values().stream().map(v -> quote + v + quote).collect(Collectors.joining(separator + "")); + + IngestDocument ingestDocument = processDocument(headers, csv); + + items.entrySet().stream().limit(3).forEach(e -> assertEquals(e.getValue(), ingestDocument.getFieldValue(e.getKey(), String.class))); + } + + public void testSingleField() throws Exception { + String[] headers = new String[]{randomAlphaOfLengthBetween(5, 10)}; + String value = randomAlphaOfLengthBetween(5, 10); + String csv = quote + value + quote; + + IngestDocument ingestDocument = processDocument(headers, csv); + + assertEquals(value, ingestDocument.getFieldValue(headers[0], String.class)); + } + + public void testEscapedQuote() throws Exception { + int numItems = randomIntBetween(2, 10); + Map items = new LinkedHashMap<>(); + for (int i = 0; i < numItems; i++) { + items.put(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(5, 10) + quote + quote + randomAlphaOfLengthBetween(5 + , 10) + quote + quote); + } + String[] headers = items.keySet().toArray(new String[numItems]); + String csv = items.values().stream().map(v -> quote + v + quote).collect(Collectors.joining(separator + "")); + + IngestDocument ingestDocument = processDocument(headers, csv); + + items.forEach((key, value) -> assertEquals(value.replace(quote + quote, quote), ingestDocument.getFieldValue(key, String.class))); + } + + public void testQuotedStrings() throws Exception { + assumeFalse("quote needed", quote.isEmpty()); + int numItems = randomIntBetween(2, 10); + Map items = new LinkedHashMap<>(); + for (int i = 0; i < numItems; i++) { + items.put(randomAlphaOfLengthBetween(5, 10), + separator + randomAlphaOfLengthBetween(5, 10) + separator + "\n\r" + randomAlphaOfLengthBetween(5, 10)); + } + String[] headers = items.keySet().toArray(new String[numItems]); + String csv = items.values().stream().map(v -> quote + v + quote).collect(Collectors.joining(separator + "")); + + IngestDocument ingestDocument = processDocument(headers, csv); + + items.forEach((key, value) -> assertEquals(value.replace(quote + quote, quote), ingestDocument.getFieldValue(key, + String.class))); + } + + public void testEmptyFields() throws Exception { + int numItems = randomIntBetween(5, 10); + Map items = new LinkedHashMap<>(); + for (int i = 0; i < numItems; i++) { + items.put(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(5, 10)); + } + String[] headers = items.keySet().toArray(new String[numItems]); + String csv = + items.values().stream().map(v -> quote + v + quote).limit(numItems - 1).skip(3).collect(Collectors.joining(separator + "")); + + IngestDocument ingestDocument = processDocument(headers, + "" + separator + "" + separator + "" + separator + csv + separator + separator + + "abc"); + + items.keySet().stream().limit(3).forEach(key -> assertFalse(ingestDocument.hasField(key))); + items.entrySet().stream().limit(numItems - 1).skip(3).forEach(e -> assertEquals(e.getValue(), + ingestDocument.getFieldValue(e.getKey(), String.class))); + items.keySet().stream().skip(numItems - 1).forEach(key -> assertFalse(ingestDocument.hasField(key))); + } + + public void testWrongStings() throws Exception { + assumeTrue("single run only", quote.isEmpty()); + expectThrows(IllegalArgumentException.class, () -> processDocument(new String[]{"a"}, "abc\"abc")); + expectThrows(IllegalArgumentException.class, () -> processDocument(new String[]{"a"}, "\"abc\"asd")); + expectThrows(IllegalArgumentException.class, () -> processDocument(new String[]{"a"}, "\"abcasd")); + expectThrows(IllegalArgumentException.class, () -> processDocument(new String[]{"a"}, "abc\nabc")); + expectThrows(IllegalArgumentException.class, () -> processDocument(new String[]{"a"}, "abc\rabc")); + } + + public void testQuotedWhitespaces() throws Exception { + assumeFalse("quote needed", quote.isEmpty()); + IngestDocument document = processDocument(new String[]{"a", "b", "c", "d"}, + " abc " + separator + " def" + separator + "ghi " + separator + " " + quote + " ooo " + quote); + assertEquals("abc", document.getFieldValue("a", String.class)); + assertEquals("def", document.getFieldValue("b", String.class)); + assertEquals("ghi", document.getFieldValue("c", String.class)); + assertEquals(" ooo ", document.getFieldValue("d", String.class)); + } + + public void testUntrimmed() throws Exception { + assumeFalse("quote needed", quote.isEmpty()); + IngestDocument document = processDocument(new String[]{"a", "b", "c", "d", "e", "f"}, + " abc " + separator + " def" + separator + "ghi " + separator + " " + + quote + "ooo" + quote + " " + separator + " " + quote + "jjj" + quote + " ", false); + assertEquals(" abc ", document.getFieldValue("a", String.class)); + assertEquals(" def", document.getFieldValue("b", String.class)); + assertEquals("ghi ", document.getFieldValue("c", String.class)); + assertEquals("ooo", document.getFieldValue("d", String.class)); + assertEquals("jjj", document.getFieldValue("e", String.class)); + assertFalse(document.hasField("f")); + } + + public void testEmptyHeaders() throws Exception { + assumeTrue("single run only", quote.isEmpty()); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random()); + String fieldName = RandomDocumentPicks.addRandomField(random(), ingestDocument, "abc,abc"); + HashMap metadata = new HashMap<>(ingestDocument.getSourceAndMetadata()); + + CsvProcessor processor = new CsvProcessor(randomAlphaOfLength(5), fieldName, new String[0], false, ',', '"', false); + + processor.execute(ingestDocument); + + assertEquals(metadata, ingestDocument.getSourceAndMetadata()); + } + + private IngestDocument processDocument(String[] headers, String csv) throws Exception { + return processDocument(headers, csv, true); + } + + private IngestDocument processDocument(String[] headers, String csv, boolean trim) throws Exception { + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random()); + + String fieldName = RandomDocumentPicks.addRandomField(random(), ingestDocument, csv); + char quoteChar = quote.isEmpty() ? '"' : quote.charAt(0); + CsvProcessor processor = new CsvProcessor(randomAlphaOfLength(5), fieldName, headers, trim, separator, quoteChar, false); + + processor.execute(ingestDocument); + + return ingestDocument; + } +} diff --git a/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/250_csv.yml b/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/250_csv.yml new file mode 100644 index 0000000000000..a38805fb1fec3 --- /dev/null +++ b/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/250_csv.yml @@ -0,0 +1,164 @@ +--- +teardown: + - do: + ingest.delete_pipeline: + id: "my_pipeline" + ignore: 404 + +--- +"Test CSV Processor defaults": + - do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "csv": { + "field": "value", + "target_fields":["a","b","c"] + } + } + ] + } + - match: { acknowledged: true } + + - do: + index: + index: test + id: 1 + pipeline: "my_pipeline" + body: > + { + "value": "aa,bb,cc" + } + + - do: + get: + index: test + id: 1 + - match: { _source.a: "aa" } + - match: { _source.b: "bb" } + - match: { _source.c: "cc" } + +--- +"Test CSV Processor quote and separator": + - do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "csv": { + "field": "value", + "target_fields":["a","b","c","d","e"], + "quote": "'", + "separator": ";" + } + } + ] + } + - match: { acknowledged: true } + + - do: + index: + index: test + id: 1 + pipeline: "my_pipeline" + body: > + { + "value": "'aa';'b;b';'cc';d,d;'ee''ee'" + } + + - do: + get: + index: test + id: 1 + - match: { _source.a: "aa" } + - match: { _source.b: "b;b" } + - match: { _source.c: "cc" } + - match: { _source.d: "d,d" } + - match: { _source.e: "ee'ee" } + +--- +"Test CSV Processor trim": + - do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "csv": { + "field": "value", + "target_fields":["a","b","c"], + "trim": true, + "quote": "'" + } + } + ] + } + - match: { acknowledged: true } + + - do: + index: + index: test + id: 1 + pipeline: "my_pipeline" + body: > + { + "value": " aa, bb , 'cc'" + } + + - do: + get: + index: test + id: 1 + - match: { _source.a: "aa" } + - match: { _source.b: "bb" } + - match: { _source.c: "cc" } + +--- +"Test CSV Processor trim log": + - do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "csv": { + "field": "value", + "target_fields":["date","level","server","id","msg"], + "trim": true, + "separator": "|" + } + } + ] + } + - match: { acknowledged: true } + + - do: + index: + index: test + id: 1 + pipeline: "my_pipeline" + body: > + { + "value": "2018-01-06 16:56:14.295748|INFO |VirtualServer |1 |listening on 0.0.0.0:9987, :::9987" + } + + - do: + get: + index: test + id: 1 + - match: { _source.date: "2018-01-06 16:56:14.295748" } + - match: { _source.level: "INFO" } + - match: { _source.server: "VirtualServer" } + - match: { _source.id: "1" } + - match: { _source.msg: "listening on 0.0.0.0:9987, :::9987" } From d43bfc1e7fa3b491868c4a0b25dfff544f786b04 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 11 Dec 2019 16:45:44 +0100 Subject: [PATCH 153/686] Fix GCS Mock Batch Delete Behavior (#50034) Batch deletes get a response for every delete request, not just those that actually hit an existing blob. The fact that we only responded for existing blobs leads to a degenerate response that throws a parse exception if a batch delete only contains non-existant blobs. --- .../GoogleCloudStorageBlobStoreRepositoryTests.java | 13 +++++++++++++ .../fixture/gcs/GoogleCloudStorageHttpHandler.java | 7 +++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java index 7a2c3d780123a..d8926b25e2c49 100644 --- a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java +++ b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java @@ -26,6 +26,8 @@ import com.sun.net.httpserver.HttpHandler; import fixture.gcs.FakeOAuth2HttpHandler; import fixture.gcs.GoogleCloudStorageHttpHandler; +import org.elasticsearch.action.ActionRunnable; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.metadata.RepositoryMetaData; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.SuppressForbidden; @@ -37,7 +39,9 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.Repository; +import org.elasticsearch.repositories.blobstore.BlobStoreRepository; import org.elasticsearch.repositories.blobstore.ESMockAPIBasedRepositoryIntegTestCase; import org.threeten.bp.Duration; @@ -101,6 +105,15 @@ protected Settings nodeSettings(int nodeOrdinal) { return settings.build(); } + public void testDeleteSingleItem() { + final String repoName = createRepository(randomName()); + final RepositoriesService repositoriesService = internalCluster().getMasterNodeInstance(RepositoriesService.class); + final BlobStoreRepository repository = (BlobStoreRepository) repositoriesService.repository(repoName); + PlainActionFuture.get(f -> repository.threadPool().generic().execute(ActionRunnable.run(f, () -> + repository.blobStore().blobContainer(repository.basePath()).deleteBlobsIgnoringIfNotExists(Collections.singletonList("foo")) + ))); + } + public void testChunkSize() { // default chunk size RepositoryMetaData repositoryMetaData = new RepositoryMetaData("repo", GoogleCloudStorageRepository.TYPE, Settings.EMPTY); diff --git a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java index ba2a725fed29b..a374a745909a5 100644 --- a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java +++ b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java @@ -167,10 +167,9 @@ public void handle(final HttpExchange exchange) throws IOException { } else if (line.startsWith("DELETE")) { final String name = line.substring(line.indexOf(uri) + uri.length(), line.lastIndexOf(" HTTP")); if (Strings.hasText(name)) { - if (blobs.entrySet().removeIf(blob -> blob.getKey().equals(URLDecoder.decode(name, UTF_8)))) { - batch.append("HTTP/1.1 204 NO_CONTENT").append('\n'); - batch.append('\n'); - } + blobs.remove(URLDecoder.decode(name, UTF_8)); + batch.append("HTTP/1.1 204 NO_CONTENT").append('\n'); + batch.append('\n'); } } } From dfe30135e997a0afa5f212724fe42e90a83ee4c3 Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Wed, 11 Dec 2019 17:18:51 +0100 Subject: [PATCH 154/686] Log attachment generation failures (#50080) Watcher logs when actions fail in ActionWrapper, but failures to generate an email attachment are not logged and we thus only know the type of the exception and not where/how it occurred. --- .../xpack/watcher/actions/email/ExecutableEmailAction.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/actions/email/ExecutableEmailAction.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/actions/email/ExecutableEmailAction.java index fcc4eb0e9422b..1f8e87cad1f95 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/actions/email/ExecutableEmailAction.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/actions/email/ExecutableEmailAction.java @@ -6,6 +6,8 @@ package org.elasticsearch.xpack.watcher.actions.email; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.xpack.core.watcher.actions.Action; import org.elasticsearch.xpack.core.watcher.actions.ExecutableAction; @@ -57,6 +59,8 @@ public Action.Result execute(String actionId, WatchExecutionContext ctx, Payload Attachment attachment = parser.toAttachment(ctx, payload, emailAttachment); attachments.put(attachment.id(), attachment); } catch (ElasticsearchException | IOException e) { + logger().error( + (Supplier) () -> new ParameterizedMessage("failed to execute action [{}/{}]", ctx.watch().id(), actionId), e); return new EmailAction.Result.FailureWithException(action.type(), e); } } From c5850ad5bef8908037086124a14138c59e0e776e Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 11 Dec 2019 09:50:41 -0800 Subject: [PATCH 155/686] [DOCS] Move datafeed resource definitions into APIs (#50005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: István Zoltán Szabó --- .../apis/datafeedresource.asciidoc | 161 ------------------ .../apis/delete-datafeed.asciidoc | 7 +- .../apis/get-datafeed-stats.asciidoc | 109 +++++++----- .../apis/get-datafeed.asciidoc | 117 +++++++++---- .../apis/preview-datafeed.asciidoc | 39 +++-- .../apis/put-datafeed.asciidoc | 66 +++---- .../apis/start-datafeed.asciidoc | 3 +- .../apis/stop-datafeed.asciidoc | 18 +- .../apis/update-datafeed.asciidoc | 83 ++++----- .../delayed-data-detection.asciidoc | 20 +-- docs/reference/ml/ml-shared.asciidoc | 153 +++++++++++++++++ docs/reference/redirects.asciidoc | 13 +- docs/reference/rest-api/defs.asciidoc | 3 - 13 files changed, 415 insertions(+), 377 deletions(-) delete mode 100644 docs/reference/ml/anomaly-detection/apis/datafeedresource.asciidoc diff --git a/docs/reference/ml/anomaly-detection/apis/datafeedresource.asciidoc b/docs/reference/ml/anomaly-detection/apis/datafeedresource.asciidoc deleted file mode 100644 index 864e71e35bdbe..0000000000000 --- a/docs/reference/ml/anomaly-detection/apis/datafeedresource.asciidoc +++ /dev/null @@ -1,161 +0,0 @@ -[role="xpack"] -[testenv="platinum"] -[[ml-datafeed-resource]] -=== {dfeed-cap} resources - -A {dfeed} resource has the following properties: - -`aggregations`:: - (object) If set, the {dfeed} performs aggregation searches. - Support for aggregations is limited and should only be used with - low cardinality data. For more information, see - {stack-ov}/ml-configuring-aggregation.html[Aggregating Data for Faster Performance]. - -`chunking_config`:: - (object) Specifies how data searches are split into time chunks. - See <>. - For example: `{"mode": "manual", "time_span": "3h"}` - -`datafeed_id`:: - (string) A numerical character string that uniquely identifies the {dfeed}. - This property is informational; you cannot change the identifier for existing - {dfeeds}. - -`frequency`:: - (time units) The interval at which scheduled queries are made while the - {dfeed} runs in real time. The default value is either the bucket span for short - bucket spans, or, for longer bucket spans, a sensible fraction of the bucket - span. For example: `150s`. - -`indices`:: - (array) An array of index names. For example: `["it_ops_metrics"]` - -`job_id`:: - (string) The unique identifier for the job to which the {dfeed} sends data. - -`query`:: - (object) The {es} query domain-specific language (DSL). This value - corresponds to the query object in an {es} search POST body. All the - options that are supported by {es} can be used, as this object is - passed verbatim to {es}. By default, this property has the following - value: `{"match_all": {"boost": 1}}`. - -`query_delay`:: - (time units) The number of seconds behind real time that data is queried. For - example, if data from 10:04 a.m. might not be searchable in {es} until - 10:06 a.m., set this property to 120 seconds. The default value is randomly - selected between `60s` and `120s`. This randomness improves the query - performance when there are multiple jobs running on the same node. - -`script_fields`:: - (object) Specifies scripts that evaluate custom expressions and returns - script fields to the {dfeed}. - The detector configuration objects in a job can contain - functions that use these script fields. - For more information, see - {stack-ov}/ml-configuring-transform.html[Transforming Data With Script Fields]. - -`scroll_size`:: - (unsigned integer) The `size` parameter that is used in {es} searches. - The default value is `1000`. - -`delayed_data_check_config`:: - (object) Specifies whether the data feed checks for missing data and - the size of the window. For example: - `{"enabled": true, "check_window": "1h"}` See - <>. - -`max_empty_searches`:: - (integer) If a real-time {dfeed} has never seen any data (including during - any initial training period) then it will automatically stop itself and - close its associated job after this many real-time searches that return no - documents. In other words, it will stop after `frequency` times - `max_empty_searches` of real-time operation. If not set - then a {dfeed} with no end time that sees no data will remain started until - it is explicitly stopped. By default this setting is not set. - -[[ml-datafeed-chunking-config]] -==== Chunking configuration objects - -{dfeeds-cap} might be required to search over long time periods, for several months -or years. This search is split into time chunks in order to ensure the load -on {es} is managed. Chunking configuration controls how the size of these time -chunks are calculated and is an advanced configuration option. - -A chunking configuration object has the following properties: - -`mode`:: - There are three available modes: + - `auto`::: The chunk size will be dynamically calculated. This is the default - and recommended value. - `manual`::: Chunking will be applied according to the specified `time_span`. - `off`::: No chunking will be applied. - -`time_span`:: - (time units) The time span that each search will be querying. - This setting is only applicable when the mode is set to `manual`. - For example: `3h`. - -[[ml-datafeed-delayed-data-check-config]] -==== Delayed data check configuration objects - -The {dfeed} can optionally search over indices that have already been read in -an effort to determine whether any data has subsequently been added to the index. -If missing data is found, it is a good indication that the `query_delay` option -is set too low and the data is being indexed after the {dfeed} has passed that -moment in time. See -{stack-ov}/ml-delayed-data-detection.html[Working with delayed data]. - -This check runs only on real-time {dfeeds}. - -The configuration object has the following properties: - -`enabled`:: - (boolean) Specifies whether the {dfeed} periodically checks for delayed data. - Defaults to `true`. - -`check_window`:: - (time units) The window of time that is searched for late data. This window of - time ends with the latest finalized bucket. It defaults to `null`, which - causes an appropriate `check_window` to be calculated when the real-time - {dfeed} runs. In particular, the default `check_window` span calculation is - based on the maximum of `2h` or `8 * bucket_span`. - -[float] -[[ml-datafeed-counts]] -==== {dfeed-cap} counts - -The get {dfeed} statistics API provides information about the operational -progress of a {dfeed}. All of these properties are informational; you cannot -update their values: - -`assignment_explanation`:: - (string) For started {dfeeds} only, contains messages relating to the - selection of a node. - -`datafeed_id`:: - (string) A numerical character string that uniquely identifies the {dfeed}. - -`node`:: - (object) The node upon which the {dfeed} is started. The {dfeed} and job will - be on the same node. - `id`::: The unique identifier of the node. For example, - "0-o0tOoRTwKFZifatTWKNw". - `name`::: The node name. For example, `0-o0tOo`. - `ephemeral_id`::: The node ephemeral ID. - `transport_address`::: The host and port where transport HTTP connections are - accepted. For example, `127.0.0.1:9300`. - `attributes`::: For example, `{"ml.machine_memory": "17179869184"}`. - -`state`:: - (string) The status of the {dfeed}, which can be one of the following values: + - `started`::: The {dfeed} is actively receiving data. - `stopped`::: The {dfeed} is stopped and will not receive data until it is - re-started. - -`timing_stats`:: - (object) An object that provides statistical information about timing aspect of this datafeed. + - `job_id`::: A numerical character string that uniquely identifies the job. - `search_count`::: Number of searches performed by this datafeed. - `total_search_time_ms`::: Total time the datafeed spent searching in milliseconds. - diff --git a/docs/reference/ml/anomaly-detection/apis/delete-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-datafeed.asciidoc index 21b4eb75bef04..d933afe4f9a42 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-datafeed.asciidoc @@ -28,14 +28,15 @@ can delete it. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {dfeed}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id] [[ml-delete-datafeed-query-parms]] ==== {api-query-parms-title} `force`:: - (Optional, boolean) Use to forcefully delete a started {dfeed}; this method is - quicker than stopping and deleting the {dfeed}. +(Optional, boolean) Use to forcefully delete a started {dfeed}; this method is +quicker than stopping and deleting the {dfeed}. [[ml-delete-datafeed-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc index bd126a651e26b..feccd52364f48 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc @@ -45,36 +45,61 @@ IMPORTANT: This API returns a maximum of 10,000 {dfeeds}. ==== {api-path-parms-title} ``:: - (Optional, string) Identifier for the {dfeed}. It can be a {dfeed} identifier - or a wildcard expression. If you do not specify one of these options, the API - returns statistics for all {dfeeds}. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id-wildcard] +If you do not specify one of these options, the API returns information about +all {dfeeds}. [[ml-get-datafeed-stats-query-parms]] ==== {api-query-parms-title} `allow_no_datafeeds`:: - (Optional, boolean) Specifies what to do when the request: -+ --- -* Contains wildcard expressions and there are no {datafeeds} that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns an empty `datafeeds` array when -there are no matches and the subset of results when there are partial matches. -If this parameter is `false`, the request returns a `404` status code when there -are no matches or only partial matches. --- +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-datafeeds] [[ml-get-datafeed-stats-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns an array of {dfeed} count objects. All of these properties are +informational; you cannot update their values. + +`assignment_explanation`:: +(string) For started {dfeeds} only, contains messages relating to the selection of a node. + +`datafeed_id`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id] + +`node`:: +(object) For started {dfeeds} only, the node upon which the {dfeed} is started. The {dfeed} and job will be on the same node. +`id`::: The unique identifier of the node. For example, "0-o0tOoRTwKFZifatTWKNw". +`name`::: The node name. For example, `0-o0tOo`. +`ephemeral_id`::: The node ephemeral ID. +`transport_address`::: The host and port where transport HTTP connections are +accepted. For example, `127.0.0.1:9300`. +`attributes`::: For example, `{"ml.machine_memory": "17179869184"}`. + +`state`:: +(string) The status of the {dfeed}, which can be one of the following values: ++ +-- +* `started`: The {dfeed} is actively receiving data. +* `stopped`: The {dfeed} is stopped and will not receive data until it is +re-started. +-- + +`timing_stats`:: +(object) An object that provides statistical information about timing aspect of +this {dfeed}. +//average_search_time_per_bucket_ms +//bucket_count +//exponential_average_search_time_per_hour_ms +`job_id`::: +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] +`search_count`::: Number of searches performed by this {dfeed}. +`total_search_time_ms`::: Total time the {dfeed} spent searching in milliseconds. -`datafeeds`:: - (array) An array of {dfeed} count objects. - For more information, see <>. [[ml-get-datafeed-stats-response-codes]] ==== {api-response-codes-title} @@ -86,46 +111,46 @@ The API returns the following information: [[ml-get-datafeed-stats-example]] ==== {api-examples-title} -The following example gets usage information for the -`datafeed-total-requests` {dfeed}: - [source,console] -------------------------------------------------- -GET _ml/datafeeds/datafeed-total-requests/_stats +GET _ml/datafeeds/datafeed-high_sum_total_sales/_stats -------------------------------------------------- -// TEST[skip:setup:server_metrics_startdf] +// TEST[skip:Kibana sample data started datafeed] The API returns the following results: [source,console-result] ---- { - "count": 1, - "datafeeds": [ + "count" : 1, + "datafeeds" : [ { - "datafeed_id": "datafeed-total-requests", - "state": "started", - "node": { - "id": "2spCyo1pRi2Ajo-j-_dnPX", - "name": "node-0", - "ephemeral_id": "hoXMLZB0RWKfR9UPPUCxXX", - "transport_address": "127.0.0.1:9300", - "attributes": { - "ml.machine_memory": "17179869184", - "ml.max_open_jobs": "20" + "datafeed_id" : "datafeed-high_sum_total_sales", + "state" : "started", + "node" : { + "id" : "7bmMXyWCRs-TuPfGJJ_yMw", + "name" : "node-0", + "ephemeral_id" : "hoXMLZB0RWKfR9UPPUCxXX", + "transport_address" : "127.0.0.1:9300", + "attributes" : { + "ml.machine_memory" : "17179869184", + "ml.max_open_jobs" : "20" } }, - "assignment_explanation": "", - "timing_stats": { - "job_id": "job-total-requests", - "search_count": 20, - "total_search_time_ms": 120.5 + "assignment_explanation" : "", + "timing_stats" : { + "job_id" : "high_sum_total_sales", + "search_count" : 7, + "bucket_count" : 743, + "total_search_time_ms" : 134.0, + "average_search_time_per_bucket_ms" : 0.180349932705249, + "exponential_average_search_time_per_hour_ms" : 11.514712961628677 } } ] } ---- -// TESTRESPONSE[s/"2spCyo1pRi2Ajo-j-_dnPX"/$body.$_path/] +// TESTRESPONSE[s/"7bmMXyWCRs-TuPfGJJ_yMw"/$body.$_path/] // TESTRESPONSE[s/"node-0"/$body.$_path/] // TESTRESPONSE[s/"hoXMLZB0RWKfR9UPPUCxXX"/$body.$_path/] // TESTRESPONSE[s/"127.0.0.1:9300"/$body.$_path/] diff --git a/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc index 3330ae7b821dc..11aca1edd95e1 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc @@ -42,35 +42,71 @@ IMPORTANT: This API returns a maximum of 10,000 {dfeeds}. ==== {api-path-parms-title} ``:: - (Optional, string) Identifier for the {dfeed}. It can be a {dfeed} identifier - or a wildcard expression. If you do not specify one of these options, the API - returns information about all {dfeeds}. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id-wildcard] +If you do not specify one of these options, the API returns information about +all {dfeeds}. [[ml-get-datafeed-query-parms]] ==== {api-query-parms-title} `allow_no_datafeeds`:: - (Optional, boolean) Specifies what to do when the request: -+ --- -* Contains wildcard expressions and there are no {datafeeds} that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns an empty `datafeeds` array when -there are no matches and the subset of results when there are partial matches. -If this parameter is `false`, the request returns a `404` status code when there -are no matches or only partial matches. --- +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-datafeeds] [[ml-get-datafeed-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns an array of {dfeed} resources, which have the following +properties: -`datafeeds`:: - (array) An array of {dfeed} objects. - For more information, see <>. +`aggregations`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=aggregations] + +`chunking_config`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=chunking-config] + +`datafeed_id`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id] + +`delayed_data_check_config`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=delayed-data-check-config] + +`frequency`:: +(<>) +include::{docdir}/ml/ml-shared.asciidoc[tag=frequency] + +`indices`:: +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=indices] + +`job_id`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-datafeed] + +`max_empty_searches`:: +(integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=max-empty-searches] + +`query`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=query] + +`query_delay`:: +(<>) +include::{docdir}/ml/ml-shared.asciidoc[tag=query-delay] + +`script_fields`:: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=script-fields] + +`scroll_size`:: +(unsigned integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=scroll-size] [[ml-get-datafeed-response-codes]] ==== {api-response-codes-title} @@ -83,39 +119,48 @@ The API returns the following information: ==== {api-examples-title} The following example gets configuration information for the -`datafeed-total-requests` {dfeed}: +`datafeed-high_sum_total_sales` {dfeed}: [source,console] -------------------------------------------------- -GET _ml/datafeeds/datafeed-total-requests +GET _ml/datafeeds/datafeed-high_sum_total_sales -------------------------------------------------- -// TEST[skip:setup:server_metrics_datafeed] +// TEST[skip:kibana sample data] The API returns the following results: [source,console-result] ---- { - "count": 1, - "datafeeds": [ + "count" : 1, + "datafeeds" : [ { - "datafeed_id": "datafeed-total-requests", - "job_id": "total-requests", - "query_delay": "83474ms", - "indices": [ - "server-metrics" + "datafeed_id" : "datafeed-high_sum_total_sales", + "job_id" : "high_sum_total_sales", + "query_delay" : "93169ms", + "indices" : [ + "kibana_sample_data_ecommerce" ], - "query": { - "match_all": { - "boost": 1.0 + "query" : { + "bool" : { + "filter" : [ + { + "term" : { + "_index" : "kibana_sample_data_ecommerce" + } + } + ] } }, - "scroll_size": 1000, - "chunking_config": { - "mode": "auto" + "scroll_size" : 1000, + "chunking_config" : { + "mode" : "auto" + }, + "delayed_data_check_config" : { + "enabled" : true } } ] } ---- -// TESTRESPONSE[s/"query.boost": "1.0"/"query.boost": $body.query.boost/] +// TESTRESPONSE[s/"query.boost": "93169ms"/"query.boost": $body.query.boost/] diff --git a/docs/reference/ml/anomaly-detection/apis/preview-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/preview-datafeed.asciidoc index c3afca8b03c62..6220d8a1de242 100644 --- a/docs/reference/ml/anomaly-detection/apis/preview-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/preview-datafeed.asciidoc @@ -41,18 +41,17 @@ it to ensure it is returning the expected data. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {dfeed}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id] [[ml-preview-datafeed-example]] ==== {api-examples-title} -The following example obtains a preview of the `datafeed-farequote` {dfeed}: - [source,console] -------------------------------------------------- -GET _ml/datafeeds/datafeed-farequote/_preview +GET _ml/datafeeds/datafeed-high_sum_total_sales/_preview -------------------------------------------------- -// TEST[skip:setup:farequote_datafeed] +// TEST[skip:set up Kibana sample data] The data that is returned for this example is as follows: @@ -60,22 +59,28 @@ The data that is returned for this example is as follows: ---- [ { - "time": 1454803200000, - "airline": "JZA", - "doc_count": 5, - "responsetime": 990.4628295898438 + "order_date" : 1574294659000, + "category.keyword" : "Men's Clothing", + "customer_full_name.keyword" : "Sultan Al Benson", + "taxful_total_price" : 35.96875 }, { - "time": 1454803200000, - "airline": "JBU", - "doc_count": 23, - "responsetime": 877.5927124023438 + "order_date" : 1574294918000, + "category.keyword" : [ + "Women's Accessories", + "Women's Clothing" + ], + "customer_full_name.keyword" : "Pia Webb", + "taxful_total_price" : 83.0 }, { - "time": 1454803200000, - "airline": "KLM", - "doc_count": 42, - "responsetime": 1355.481201171875 + "order_date" : 1574295782000, + "category.keyword" : [ + "Women's Accessories", + "Women's Shoes" + ], + "customer_full_name.keyword" : "Brigitte Graham", + "taxful_total_price" : 72.0 } ] ---- diff --git a/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc index ca3b9d61ba7a1..cb3765a86c97e 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc @@ -43,70 +43,52 @@ those same roles. ==== {api-path-parms-title} ``:: - (Required, string) A numerical character string that uniquely identifies the - {dfeed}. This identifier can contain lowercase alphanumeric characters (a-z - and 0-9), hyphens, and underscores. It must start and end with alphanumeric - characters. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id] [[ml-put-datafeed-request-body]] ==== {api-request-body-title} `aggregations`:: - (Optional, object) If set, the {dfeed} performs aggregation searches. For more - information, see <>. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=aggregations] `chunking_config`:: - (Optional, object) Specifies how data searches are split into time chunks. See - <>. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=chunking-config] `delayed_data_check_config`:: - (Optional, object) Specifies whether the data feed checks for missing data and - the size of the window. See <>. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=delayed-data-check-config] `frequency`:: - (Optional, <>) The interval at which scheduled queries - are made while the {dfeed} runs in real time. The default value is either the - bucket span for short bucket spans, or, for longer bucket spans, a sensible - fraction of the bucket span. For example: `150s`. +(Optional, <>) +include::{docdir}/ml/ml-shared.asciidoc[tag=frequency] `indices`:: - (Required, array) An array of index names. Wildcards are supported. For - example: `["it_ops_metrics", "server*"]`. -+ --- -NOTE: If any indices are in remote clusters then `cluster.remote.connect` must -not be set to `false` on any ML node. --- +(Required, array) +include::{docdir}/ml/ml-shared.asciidoc[tag=indices] -`job_id`:: - (Required, string) A numerical character string that uniquely identifies the - {anomaly-job}. +`job_id`:: +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + `query`:: - (Optional, object) The {es} query domain-specific language (DSL). This value - corresponds to the query object in an {es} search POST body. All the options - that are supported by {Es} can be used, as this object is passed verbatim to - {es}. By default, this property has the following value: - `{"match_all": {"boost": 1}}`. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=query] `query_delay`:: - (Optional, <>) The number of seconds behind real time - that data is queried. For example, if data from 10:04 a.m. might not be - searchable in {es} until 10:06 a.m., set this property to 120 seconds. The - default value is `60s`. +(Optional, <>) +include::{docdir}/ml/ml-shared.asciidoc[tag=query-delay] `script_fields`:: - (Optional, object) Specifies scripts that evaluate custom expressions and - returns script fields to the {dfeed}. The detector configuration objects in a - job can contain functions that use these script fields. For more information, - see <>. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=script-fields] `scroll_size`:: - (Optional, unsigned integer) The `size` parameter that is used in {es} - searches. The default value is `1000`. - -For more information about these properties, -see <>. +(Optional, unsigned integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=scroll-size] [[ml-put-datafeed-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/start-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/start-datafeed.asciidoc index 7faba863774dc..dd3e6bbdfff54 100644 --- a/docs/reference/ml/anomaly-detection/apis/start-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/start-datafeed.asciidoc @@ -74,7 +74,8 @@ creation/update and runs the query using those same roles. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {dfeed}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id] [[ml-start-datafeed-request-body]] ==== {api-request-body-title} diff --git a/docs/reference/ml/anomaly-detection/apis/stop-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/stop-datafeed.asciidoc index cde9f16c384af..f115d8657f7ec 100644 --- a/docs/reference/ml/anomaly-detection/apis/stop-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/stop-datafeed.asciidoc @@ -40,25 +40,15 @@ comma-separated list of {dfeeds} or a wildcard expression. You can close all ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {dfeed}. It can be a {dfeed} identifier - or a wildcard expression. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id-wildcard] [[ml-stop-datafeed-query-parms]] ==== {api-query-parms-title} `allow_no_datafeeds`:: - (Optional, boolean) Specifies what to do when the request: -+ --- -* Contains wildcard expressions and there are no {datafeeds} that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns an empty `datafeeds` array when -there are no matches and the subset of results when there are partial matches. -If this parameter is `false`, the request returns a `404` status code when there -are no matches or only partial matches. --- +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-datafeeds] [[ml-stop-datafeed-request-body]] ==== {api-request-body-title} diff --git a/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc index d201d6cd093b2..1336f71fcff77 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc @@ -39,7 +39,8 @@ using those same roles. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {dfeed}. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id] [[ml-update-datafeed-request-body]] ==== {api-request-body-title} @@ -47,70 +48,58 @@ using those same roles. The following properties can be updated after the {dfeed} is created: `aggregations`:: - (Optional, object) If set, the {dfeed} performs aggregation searches. For more - information, see <>. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=aggregations] `chunking_config`:: - (Optional, object) Specifies how data searches are split into time chunks. See - <>. - +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=chunking-config] + `delayed_data_check_config`:: - (Optional, object) Specifies whether the data feed checks for missing data and - the size of the window. See <>. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=delayed-data-check-config] `frequency`:: - (Optional, <>) The interval at which scheduled queries - are made while the {dfeed} runs in real time. The default value is either the - bucket span for short bucket spans, or, for longer bucket spans, a sensible - fraction of the bucket span. For example: `150s`. +(Optional, <>) +include::{docdir}/ml/ml-shared.asciidoc[tag=frequency] `indices`:: - (Optional, array) An array of index names. Wildcards are supported. For - example: `["it_ops_metrics", "server*"]`. +(Optional, array) +include::{docdir}/ml/ml-shared.asciidoc[tag=indices] + +`max_empty_searches`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=max-empty-searches] ++ +-- +The special value `-1` unsets this setting. +-- `query`:: - (Optional, object) The {es} query domain-specific language (DSL). This value - corresponds to the query object in an {es} search POST body. All the options - that are supported by {es} can be used, as this object is passed verbatim to - {es}. By default, this property has the following value: - `{"match_all": {"boost": 1}}`. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=query] + -- -WARNING: If you change the query, then the analyzed data will also be changed, -therefore the required time to learn might be long and the understandability of -the results is unpredictable. -If you want to make significant changes to the source data, we would recommend -you clone it and create a second job containing the amendments. Let both run in -parallel and close one when you are satisfied with the results of the other job. +WARNING: If you change the query, the analyzed data is also changed. Therefore, +the required time to learn might be long and the understandability of the +results is unpredictable. If you want to make significant changes to the source +data, we would recommend you clone it and create a second job containing the +amendments. Let both run in parallel and close one when you are satisfied with +the results of the other job. + -- `query_delay`:: - (Optional, <>) The number of seconds behind real-time - that data is queried. For example, if data from 10:04 a.m. might not be - searchable in {es} until 10:06 a.m., set this property to 120 seconds. The - default value is `60s`. +(Optional, <>) +include::{docdir}/ml/ml-shared.asciidoc[tag=query-delay] `script_fields`:: - (Optional, object) Specifies scripts that evaluate custom expressions and - returns script fields to the {dfeed}. The detector configuration objects in a - job can contain functions that use these script fields. For more information, - see <>. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=script-fields] `scroll_size`:: - (Optional, unsigned integer) The `size` parameter that is used in {es} - searches. The default value is `1000`. - -`max_empty_searches`:: - (Optional, integer) If a real-time {dfeed} has never seen any data (including - during any initial training period) then it will automatically stop itself - and close its associated job after this many real-time searches that return - no documents. In other words, it will stop after `frequency` times - `max_empty_searches` of real-time operation. If not set - then a {dfeed} with no end time that sees no data will remain started until - it is explicitly stopped. The special value `-1` unsets this setting. - -For more information about these properties, see <>. - +(Optional, unsigned integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=scroll-size] [[ml-update-datafeed-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/delayed-data-detection.asciidoc b/docs/reference/ml/anomaly-detection/delayed-data-detection.asciidoc index 625f839a86834..53f1756a4ec92 100644 --- a/docs/reference/ml/anomaly-detection/delayed-data-detection.asciidoc +++ b/docs/reference/ml/anomaly-detection/delayed-data-detection.asciidoc @@ -5,14 +5,15 @@ Delayed data are documents that are indexed late. That is to say, it is data related to a time that the {dfeed} has already processed. -When you create a datafeed, you can specify a -{ref}/ml-datafeed-resource.html[`query_delay`] setting. This setting enables the -datafeed to wait for some time past real-time, which means any "late" data in -this period is fully indexed before the datafeed tries to gather it. However, if -the setting is set too low, the datafeed may query for data before it has been -indexed and consequently miss that document. Conversely, if it is set too high, -analysis drifts farther away from real-time. The balance that is struck depends -upon each use case and the environmental factors of the cluster. +When you create a {dfeed}, you can specify a +{ref}/ml-put-datafeed.html#ml-put-datafeed-request-body[`query_delay`] setting. +This setting enables the {dfeed} to wait for some time past real-time, which +means any "late" data in this period is fully indexed before the {dfeed} tries +to gather it. However, if the setting is set too low, the {dfeed} may query for +data before it has been indexed and consequently miss that document. Conversely, +if it is set too high, analysis drifts farther away from real-time. The balance +that is struck depends upon each use case and the environmental factors of the +cluster. ==== Why worry about delayed data? @@ -28,8 +29,7 @@ recorded so that you can determine a next course of action. ==== How do we detect delayed data? -In addition to the `query_delay` field, there is a -{ref}/ml-datafeed-resource.html#ml-datafeed-delayed-data-check-config[delayed data check config], +In addition to the `query_delay` field, there is a delayed data check config, which enables you to configure the datafeed to look in the past for delayed data. Every 15 minutes or every `check_window`, whichever is smaller, the datafeed triggers a document search over the configured indices. This search looks over a diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index bea970078d06b..f277e6ab2e4ad 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -1,3 +1,10 @@ +tag::aggregations[] +If set, the {dfeed} performs aggregation searches. Support for aggregations is +limited and should only be used with low cardinality data. For more information, +see +{stack-ov}/ml-configuring-aggregation.html[Aggregating data for faster performance]. +end::aggregations[] + tag::allow-lazy-open[] Advanced configuration option. Specifies whether this job can open when there is insufficient {ml} node capacity for it to be immediately assigned to a node. The @@ -9,6 +16,21 @@ return an error and the job waits in the `opening` state until sufficient {ml} node capacity is available. end::allow-lazy-open[] +tag::allow-no-datafeeds[] +Specifies what to do when the request: ++ +-- +* Contains wildcard expressions and there are no {dfeeds} that match. +* Contains the `_all` string or no identifiers and there are no matches. +* Contains wildcard expressions and there are only partial matches. + +The default value is `true`, which returns an empty `datafeeds` array when +there are no matches and the subset of results when there are partial matches. +If this parameter is `false`, the request returns a `404` status code when there +are no matches or only partial matches. +-- +end::allow-no-datafeeds[] + tag::allow-no-jobs[] Specifies what to do when the request: + @@ -207,6 +229,22 @@ add them here as <>. end::char-filter[] +tag::chunking-config[] +{dfeeds-cap} might be required to search over long time periods, for several months +or years. This search is split into time chunks in order to ensure the load +on {es} is managed. Chunking configuration controls how the size of these time +chunks are calculated and is an advanced configuration option. +A chunking configuration object has the following properties: + +`mode`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=mode] + +`time_span`::: +(<>) +include::{docdir}/ml/ml-shared.asciidoc[tag=time-span] +end::chunking-config[] + tag::custom-rules[] An array of custom rule objects, which enable you to customize the way detectors operate. For example, a rule may dictate to the detector conditions under which @@ -301,6 +339,47 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=time-format] -- end::data-description[] +tag::datafeed-id[] +A numerical character string that uniquely identifies the +{dfeed}. This identifier can contain lowercase alphanumeric characters (a-z +and 0-9), hyphens, and underscores. It must start and end with alphanumeric +characters. +end::datafeed-id[] + +tag::datafeed-id-wildcard[] +Identifier for the {dfeed}. It can be a {dfeed} identifier or a wildcard +expression. +end::datafeed-id-wildcard[] + +tag::delayed-data-check-config[] +Specifies whether the {dfeed} checks for missing data and the size of the +window. For example: `{"enabled": true, "check_window": "1h"}`. ++ +-- +The {dfeed} can optionally search over indices that have already been read in +an effort to determine whether any data has subsequently been added to the index. +If missing data is found, it is a good indication that the `query_delay` option +is set too low and the data is being indexed after the {dfeed} has passed that +moment in time. See +{stack-ov}/ml-delayed-data-detection.html[Working with delayed data]. + +This check runs only on real-time {dfeeds}. + +The configuration object has the following properties: + +`enabled`:: +(boolean) Specifies whether the {dfeed} periodically checks for delayed data. +Defaults to `true`. + +`check_window`:: +(<>) The window of time that is searched for late data. +This window of time ends with the latest finalized bucket. It defaults to +`null`, which causes an appropriate `check_window` to be calculated when the +real-time {dfeed} runs. In particular, the default `check_window` span +calculation is based on the maximum of `2h` or `8 * bucket_span`. +-- +end::delayed-data-check-config[] + tag::dependent_variable[] `dependent_variable`:: (Required, string) Defines which field of the document is to be predicted. @@ -405,6 +484,13 @@ optional. If it is not specified, no token filters are applied prior to categorization. end::filter[] +tag::frequency[] +The interval at which scheduled queries are made while the {dfeed} runs in real +time. The default value is either the bucket span for short bucket spans, or, +for longer bucket spans, a sensible fraction of the bucket span. For example: +`150s`. +end::frequency[] + tag::function[] The analysis function that is used. For example, `count`, `rare`, `mean`, `min`, `max`, and `sum`. For more information, see @@ -424,6 +510,17 @@ tag::groups[] A list of job groups. A job can belong to no groups or many. end::groups[] +tag::indices[] +An array of index names. Wildcards are supported. For example: +`["it_ops_metrics", "server*"]`. ++ +-- +NOTE: If any indices are in remote clusters then `cluster.remote.connect` must +not be set to `false` on any {ml} nodes. + +-- +end::indices[] + tag::influencers[] A comma separated list of influencer field names. Typically these can be the by, over, or partition fields that are used in the detector configuration. You might @@ -475,6 +572,10 @@ alphanumeric characters (a-z and 0-9), hyphens, and underscores. It must start and end with alphanumeric characters. end::job-id-data-frame-analytics-define[] +tag::job-id-datafeed[] +The unique identifier for the job to which the {dfeed} sends data. +end::job-id-datafeed[] + tag::jobs-stats-anomaly-detection[] An array of {anomaly-job} statistics objects. For more information, see <>. @@ -502,12 +603,32 @@ the <> API. -- end::latency[] +tag::max-empty-searches[] +If a real-time {dfeed} has never seen any data (including during any initial +training period) then it will automatically stop itself and close its associated +job after this many real-time searches that return no documents. In other words, +it will stop after `frequency` times `max_empty_searches` of real-time operation. +If not set then a {dfeed} with no end time that sees no data will remain started +until it is explicitly stopped. By default this setting is not set. +end::max-empty-searches[] + tag::maximum_number_trees[] `maximum_number_trees`:: (Optional, integer) Defines the maximum number of trees the forest is allowed to contain. The maximum value is 2000. end::maximum_number_trees[] +tag::mode[] +There are three available modes: ++ +-- +* `auto`: The chunk size is dynamically calculated. This is the default and +recommended value. +* `manual`: Chunking is applied according to the specified `time_span`. +* `off`: No chunking is applied. +-- +end::mode[] + tag::model-memory-limit[] The approximate maximum amount of memory resources that are required for analytical processing. Once this limit is approached, data pruning becomes @@ -615,6 +736,21 @@ tag::prediction_field_name[] Defaults to `_prediction`. end::prediction_field_name[] +tag::query[] +The {es} query domain-specific language (DSL). This value corresponds to the +query object in an {es} search POST body. All the options that are supported by +{es} can be used, as this object is passed verbatim to {es}. By default, this +property has the following value: `{"match_all": {"boost": 1}}`. +end::query[] + +tag::query-delay[] +The number of seconds behind real time that data is queried. For example, if +data from 10:04 a.m. might not be searchable in {es} until 10:06 a.m., set this +property to 120 seconds. The default value is randomly selected between `60s` +and `120s`. This randomness improves the query performance when there are +multiple jobs running on the same node. +end::query-delay[] + tag::renormalization-window-days[] Advanced configuration option. The period over which adjustments to the score are applied, as new data is seen. The default value is the longer of 30 days or @@ -633,6 +769,18 @@ are deleted from {es}. The default value is null, which means results are retained. end::results-retention-days[] +tag::script-fields[] +Specifies scripts that evaluate custom expressions and returns script fields to +the {dfeed}. The detector configuration objects in a job can contain functions +that use these script fields. For more information, see +{stack-ov}/ml-configuring-transform.html[Transforming data with script fields] +and <>. +end::script-fields[] + +tag::scroll-size[] +The `size` parameter that is used in {es} searches. The default value is `1000`. +end::scroll-size[] + tag::summary-count-field-name[] If this property is specified, the data that is fed to the job is expected to be pre-summarized. This property value is the name of the field that contains the @@ -663,6 +811,11 @@ job creation fails. -- end::time-format[] +tag::time-span[] +The time span that each search will be querying. This setting is only applicable +when the mode is set to `manual`. For example: `3h`. +end::time-span[] + tag::tokenizer[] The name or definition of the <> to use after character filters are applied. This property is compulsory if diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 26694e9aa05ab..afc10a4493b17 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -1054,4 +1054,15 @@ This page was deleted. [[ml-analysisconfig]] See the details in [[ml-apimodelplotconfig]] -<>, <>, and <>. \ No newline at end of file +<>, <>, and <>. + +[role="exclude",id="ml-datafeed-resource"] +=== {dfeed-cap} resources + +This page was deleted. +[[ml-datafeed-chunking-config]] +See the details in <>, <>, +[[ml-datafeed-delayed-data-check-config]] +<>, +[[ml-datafeed-counts]] +<>. \ No newline at end of file diff --git a/docs/reference/rest-api/defs.asciidoc b/docs/reference/rest-api/defs.asciidoc index ec1a5a0e4154f..8bdf35e62f11b 100644 --- a/docs/reference/rest-api/defs.asciidoc +++ b/docs/reference/rest-api/defs.asciidoc @@ -5,8 +5,6 @@ These resource definitions are used in APIs related to {ml-features} and {security-features} and in {kib} advanced {ml} job configuration options. -* <> -* <> * <> * <> * <> @@ -15,7 +13,6 @@ These resource definitions are used in APIs related to {ml-features} and * <> * <> -include::{es-repo-dir}/ml/anomaly-detection/apis/datafeedresource.asciidoc[] include::{es-repo-dir}/ml/df-analytics/apis/dfanalyticsresources.asciidoc[] include::{es-repo-dir}/ml/df-analytics/apis/evaluateresources.asciidoc[] include::{es-repo-dir}/ml/anomaly-detection/apis/jobcounts.asciidoc[] From 31fe7000a9b700c6112da65165a497ed5351d78b Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Wed, 11 Dec 2019 11:04:26 -0800 Subject: [PATCH 156/686] Fix exception message for boolean BuildParams properties (#50094) --- .../java/org/elasticsearch/gradle/info/BuildParams.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/minimumRuntime/java/org/elasticsearch/gradle/info/BuildParams.java b/buildSrc/src/minimumRuntime/java/org/elasticsearch/gradle/info/BuildParams.java index 9fba20c7db77c..19748de1b96f1 100644 --- a/buildSrc/src/minimumRuntime/java/org/elasticsearch/gradle/info/BuildParams.java +++ b/buildSrc/src/minimumRuntime/java/org/elasticsearch/gradle/info/BuildParams.java @@ -137,7 +137,7 @@ private static T value(T object) { } private static String propertyName(String methodName) { - String propertyName = methodName.substring("get".length()); + String propertyName = methodName.startsWith("is") ? methodName.substring("is".length()) : methodName.substring("get".length()); return propertyName.substring(0, 1).toLowerCase() + propertyName.substring(1); } From d87de8e7251b9c26afab5c9b57e0be40b48c45a6 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 11 Dec 2019 11:17:15 -0800 Subject: [PATCH 157/686] [DOCS] Move job count resource definitions into API (#50057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: PrzemysÅ‚aw Witek Co-Authored-By: David Roberts Co-Authored-By: Ed Savage <32410745+edsavage@users.noreply.github.com> --- .../apis/get-job-stats.asciidoc | 396 ++++++++++++++++-- .../anomaly-detection/apis/jobcounts.asciidoc | 260 ------------ .../anomaly-detection/apis/post-data.asciidoc | 8 +- docs/reference/ml/ml-shared.asciidoc | 5 - docs/reference/redirects.asciidoc | 18 +- docs/reference/rest-api/defs.asciidoc | 2 - 6 files changed, 371 insertions(+), 318 deletions(-) delete mode 100644 docs/reference/ml/anomaly-detection/apis/jobcounts.asciidoc diff --git a/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc index 99928e43ca5f3..4ef7704fe9fd0 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc @@ -53,11 +53,274 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-jobs] [[ml-get-job-stats-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns the following information about the operational progress of a +job: -`jobs`:: -(array) -include::{docdir}/ml/ml-shared.asciidoc[tag=jobs-stats-anomaly-detection] +`assignment_explanation`:: +(string) For open jobs only, contains messages relating to the selection +of a node to run the job. + +[[datacounts]]`data_counts`:: +(object) An object that describes the quantity of input to the job and any +related error counts. The `data_count` values are cumulative for the lifetime of +a job. If a model snapshot is reverted or old results are deleted, the job +counts are not reset. + +`bucket_count`::: +(long) The number of bucket results produced by the job. + +`earliest_record_timestamp`::: +(date) The timestamp of the earliest chronologically input document. + +`empty_bucket_count`::: +(long) The number of buckets which did not contain any data. If your data +contains many empty buckets, consider increasing your `bucket_span` or using +functions that are tolerant to gaps in data such as `mean`, `non_null_sum` or +`non_zero_count`. + +`input_bytes`::: +(long) The number of bytes of input data posted to the job. + +`input_field_count`::: +(long) The total number of fields in input documents posted to the job. This +count includes fields that are not used in the analysis. However, be aware that +if you are using a {dfeed}, it extracts only the required fields from the +documents it retrieves before posting them to the job. + +`input_record_count`::: +(long) The number of input documents posted to the job. + +`invalid_date_count`::: +(long) The number of input documents with either a missing date field or a date +that could not be parsed. + +`job_id`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +`last_data_time`::: +(date) The timestamp at which data was last analyzed, according to server time. + +`latest_empty_bucket_timestamp`::: +(date) The timestamp of the last bucket that did not contain any data. + +`latest_record_timestamp`::: +(date) The timestamp of the latest chronologically input document. + +`latest_sparse_bucket_timestamp`::: +(date) The timestamp of the last bucket that was considered sparse. + +`missing_field_count`::: +(long) The number of input documents that are missing a field that the job is +configured to analyze. Input documents with missing fields are still processed +because it is possible that not all fields are missing. The value of +`processed_record_count` includes this count. ++ +-- +NOTE: If you are using {dfeeds} or posting data to the job in JSON format, a +high `missing_field_count` is often not an indication of data issues. It is not +necessarily a cause for concern. + +-- + +`out_of_order_timestamp_count`::: +(long) The number of input documents that are out of time sequence and outside +of the latency window. This information is applicable only when you provide data +to the job by using the <>. These out of order +documents are discarded, since jobs require time series data to be in ascending +chronological order. + +`processed_field_count`::: +(long) The total number of fields in all the documents that have been processed +by the job. Only fields that are specified in the detector configuration object +contribute to this count. The timestamp is not included in this count. + +`processed_record_count`::: +(long) The number of input documents that have been processed by the job. This +value includes documents with missing fields, since they are nonetheless +analyzed. If you use {dfeeds} and have aggregations in your search query, the +`processed_record_count` will be the number of aggregation results processed, +not the number of {es} documents. + +`sparse_bucket_count`::: +(long) The number of buckets that contained few data points compared to the +expected number of data points. If your data contains many sparse buckets, +consider using a longer `bucket_span`. + +[[forecastsstats]]`forecasts_stats`:: +(object) An object that provides statistical information about forecasts +of this job. It has the following properties: ++ +-- +NOTE: Unless there is at least one forecast, `memory_bytes`, `records`, +`processing_time_ms` and `status` properties are omitted. + +-- + +`forecasted_jobs`::: +(long) The number of jobs that have at least one forecast. + +`memory_bytes`::: +(object) Statistics about the memory usage: minimum, maximum, average and total. + +`records`::: +(object) Statistics about the number of forecast records: minimum, maximum, +saverage and total. + +`processing_time_ms`::: +(object) Statistics about the forecast runtime in milliseconds: minimum, maximum, +average and total. + +`status`::: +(object) Counts per forecast status. For example: `{"finished" : 2}`. + +`total`::: +(long) The number of forecasts currently available for this model. + +`job_id`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +[[modelsizestats]]`model_size_stats`:: +(object) An object that provides information about the size and contents of the +model. It has the following properties: + +`bucket_allocation_failures_count`::: +(long) The number of buckets for which new entities in incoming data were not +processed due to insufficient model memory. This situation is also signified +by a `hard_limit: memory_status` property value. + +`job_id`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +`log_time`::: +(date) The timestamp of the `model_size_stats` according to server time. + +`memory_status`::: +(string) The status of the mathematical models. This property can have one of +the following values: ++ +-- +* `ok`: The models stayed below the configured value. +* `soft_limit`: The models used more than 60% of the configured memory limit +and older unused models will be pruned to free up space. +* `hard_limit`: The models used more space than the configured memory limit. +As a result, not all incoming data was processed. +-- + +`model_bytes`::: +(long) The number of bytes of memory used by the models. This is the maximum +value since the last time the model was persisted. If the job is closed, +this value indicates the latest size. + +`model_bytes_exceeded`::: + (long) The number of bytes over the high limit for memory usage at the last + allocation failure. + +`model_bytes_memory_limit`::: +(long) The upper limit for memory usage, checked on increasing values. + +`result_type`::: +(string) For internal use. The type of result. + +`total_by_field_count`::: +(long) The number of `by` field values that were analyzed by the models. ++ +-- +NOTE: The `by` field values are counted separately for each detector and +partition. + +-- + +`total_over_field_count`::: +(long) The number of `over` field values that were analyzed by the models. ++ +-- +NOTE: The `over` field values are counted separately for each detector and +partition. + +-- + +`total_partition_field_count`::: +(long) The number of `partition` field values that were analyzed by the models. + +`timestamp`::: +(date) The timestamp of the `model_size_stats` according to the timestamp of the +data. + +[[stats-node]]`node`:: +(object) Contains properties for the node that runs the job. This information is +available only for open jobs. + +`attributes`::: +(object) Lists node attributes. For example, +`{"ml.machine_memory": "17179869184", "ml.max_open_jobs" : "20"}`. + +`ephemeral_id`::: +(string) The ephemeral ID of the node. + +`id`::: +(string) The unique identifier of the node. + +`name`::: +(string) The node name. + +`transport_address`::: +(string) The host and port where transport HTTP connections are accepted. + +`open_time`:: +(string) For open jobs only, the elapsed time for which the job has been open. +For example, `28746386s`. + +`state`:: +(string) The status of the job, which can be one of the following values: ++ +-- +* `closed`: The job finished successfully with its model state persisted. The +job must be opened before it can accept further data. +* `closing`: The job close action is in progress and has not yet completed. A +closing job cannot accept further data. +* `failed`: The job did not finish successfully due to an error. This situation +can occur due to invalid input data, a fatal error occurring during the analysis, +or an external interaction such as the process being killed by the Linux out of +memory (OOM) killer. If the job had irrevocably failed, it must be force closed +and then deleted. If the {dfeed} can be corrected, the job can be closed and +then re-opened. +* `opened`: The job is available to receive and process data. +* `opening`: The job open action is in progress and has not yet completed. +-- + +[[timingstats]]`timing_stats`:: +(object) An object that provides statistical information about timing aspect of +this job. It has the following properties: + +`average_bucket_processing_time_ms`::: +(double) Average of all bucket processing times in milliseconds. + +`bucket_count`::: +(long) The number of buckets processed. + +`exponential_average_bucket_processing_time_ms`::: +(double) Exponential moving average of all bucket processing times in +milliseconds. + +`exponential_average_bucket_processing_time_per_hour_ms`::: +(double) Exponentially-weighted moving average of bucket processing times +calculated in a 1 hour time window. + +`job_id`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +`maximum_bucket_processing_time_ms`::: +(double) Maximum among all bucket processing times in milliseconds. + +`minimum_bucket_processing_time_ms`::: +(double) Minimum among all bucket processing times in milliseconds. + +`total_bucket_processing_time_ms`::: +(double) Sum of all bucket processing times in milliseconds. [[ml-get-job-stats-response-codes]] ==== {api-response-codes-title} @@ -69,59 +332,100 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=jobs-stats-anomaly-detection] [[ml-get-job-stats-example]] ==== {api-examples-title} -The following example gets usage information for the `farequote` job: - [source,console] -------------------------------------------------- -GET _ml/anomaly_detectors/farequote/_stats +GET _ml/anomaly_detectors/low_request_rate/_stats -------------------------------------------------- -// TEST[skip:todo] +// TEST[skip:Kibana sample data] The API returns the following results: [source,js] ---- { - "count": 1, - "jobs": [ + "count" : 1, + "jobs" : [ { - "job_id": "farequote", - "data_counts": { - "job_id": "farequote", - "processed_record_count": 86275, - "processed_field_count": 172550, - "input_bytes": 6744714, - "input_field_count": 172550, - "invalid_date_count": 0, - "missing_field_count": 0, - "out_of_order_timestamp_count": 0, - "empty_bucket_count": 0, - "sparse_bucket_count": 15, - "bucket_count": 1528, - "earliest_record_timestamp": 1454803200000, - "latest_record_timestamp": 1455235196000, - "last_data_time": 1491948163685, - "latest_sparse_bucket_timestamp": 1455174900000, - "input_record_count": 86275 + "job_id" : "low_request_rate", + "data_counts" : { + "job_id" : "low_request_rate", + "processed_record_count" : 1216, + "processed_field_count" : 1216, + "input_bytes" : 51678, + "input_field_count" : 1216, + "invalid_date_count" : 0, + "missing_field_count" : 0, + "out_of_order_timestamp_count" : 0, + "empty_bucket_count" : 242, + "sparse_bucket_count" : 0, + "bucket_count" : 1457, + "earliest_record_timestamp" : 1575172659612, + "latest_record_timestamp" : 1580417369440, + "last_data_time" : 1576017595046, + "latest_empty_bucket_timestamp" : 1580356800000, + "input_record_count" : 1216 + }, + "model_size_stats" : { + "job_id" : "low_request_rate", + "result_type" : "model_size_stats", + "model_bytes" : 41480, + "model_bytes_exceeded" : 0, + "model_bytes_memory_limit" : 10485760, + "total_by_field_count" : 3, + "total_over_field_count" : 0, + "total_partition_field_count" : 2, + "bucket_allocation_failures_count" : 0, + "memory_status" : "ok", + "log_time" : 1576017596000, + "timestamp" : 1580410800000 + }, + "forecasts_stats" : { + "total" : 1, + "forecasted_jobs" : 1, + "memory_bytes" : { + "total" : 9179.0, + "min" : 9179.0, + "avg" : 9179.0, + "max" : 9179.0 + }, + "records" : { + "total" : 168.0, + "min" : 168.0, + "avg" : 168.0, + "max" : 168.0 + }, + "processing_time_ms" : { + "total" : 40.0, + "min" : 40.0, + "avg" : 40.0, + "max" : 40.0 + }, + "status" : { + "finished" : 1 + } }, - "model_size_stats": { - "job_id": "farequote", - "result_type": "model_size_stats", - "model_bytes": 387594, - "total_by_field_count": 21, - "total_over_field_count": 0, - "total_partition_field_count": 20, - "bucket_allocation_failures_count": 0, - "memory_status": "ok", - "log_time": 1491948163000, - "timestamp": 1455234600000 + "state" : "opened", + "node" : { + "id" : "7bmMXyWCRs-TuPfGJJ_yMw", + "name" : "node-0", + "ephemeral_id" : "hoXMLZB0RWKfR9UPPUCxXX", + "transport_address" : "127.0.0.1:9300", + "attributes" : { + "ml.machine_memory" : "17179869184", + "xpack.installed" : "true", + "ml.max_open_jobs" : "20" + } }, - "state": "closed", - "timing_stats": { - "job_id": "farequote", - "minimum_bucket_processing_time_ms": 0.0, - "maximum_bucket_processing_time_ms": 15.0, - "average_bucket_processing_time_ms": 8.75, - "exponential_average_bucket_processing_time_ms": 6.1435899 + "assignment_explanation" : "", + "open_time" : "13s", + "timing_stats" : { + "job_id" : "low_request_rate", + "bucket_count" : 1457, + "total_bucket_processing_time_ms" : 1094.000000000001, + "minimum_bucket_processing_time_ms" : 0.0, + "maximum_bucket_processing_time_ms" : 48.0, + "average_bucket_processing_time_ms" : 0.75085792724777, + "exponential_average_bucket_processing_time_ms" : 0.5571716855800993, + "exponential_average_bucket_processing_time_per_hour_ms" : 15.0 } } ] diff --git a/docs/reference/ml/anomaly-detection/apis/jobcounts.asciidoc b/docs/reference/ml/anomaly-detection/apis/jobcounts.asciidoc deleted file mode 100644 index 9f205d6f614bf..0000000000000 --- a/docs/reference/ml/anomaly-detection/apis/jobcounts.asciidoc +++ /dev/null @@ -1,260 +0,0 @@ -[role="xpack"] -[testenv="platinum"] -[[ml-jobstats]] -=== Job statistics - -The get job statistics API provides information about the operational -progress of a job. - -`assignment_explanation`:: - (string) For open jobs only, contains messages relating to the selection - of a node to run the job. - -`data_counts`:: - (object) An object that describes the number of records processed and - any related error counts. See <>. - -`job_id`:: - (string) A unique identifier for the job. - -`model_size_stats`:: - (object) An object that provides information about the size and contents of the model. - See <>. - -`forecasts_stats`:: - (object) An object that provides statistical information about forecasts - of this job. See <>. - -`timing_stats`:: - (object) An object that provides statistical information about timing aspect - of this job. See <>. - -`node`:: - (object) For open jobs only, contains information about the node where the - job runs. See <>. - -`open_time`:: - (string) For open jobs only, the elapsed time for which the job has been open. - For example, `28746386s`. - -`state`:: - (string) The status of the job, which can be one of the following values: - - `opened`::: The job is available to receive and process data. - `closed`::: The job finished successfully with its model state persisted. - The job must be opened before it can accept further data. - `closing`::: The job close action is in progress and has not yet completed. - A closing job cannot accept further data. - `failed`::: The job did not finish successfully due to an error. - This situation can occur due to invalid input data. - If the job had irrevocably failed, it must be force closed and then deleted. - If the {dfeed} can be corrected, the job can be closed and then re-opened. - `opening`::: The job open action is in progress and has not yet completed. - -[float] -[[ml-datacounts]] -==== Data Counts Objects - -The `data_counts` object describes the number of records processed -and any related error counts. - -The `data_count` values are cumulative for the lifetime of a job. If a model snapshot is reverted -or old results are deleted, the job counts are not reset. - -`bucket_count`:: - (long) The number of bucket results produced by the job. - -`earliest_record_timestamp`:: - (date) The timestamp of the earliest chronologically input document. - -`empty_bucket_count`:: - (long) The number of buckets which did not contain any data. If your data contains many - empty buckets, consider increasing your `bucket_span` or using functions that are tolerant - to gaps in data such as `mean`, `non_null_sum` or `non_zero_count`. - -`input_bytes`:: - (long) The number of raw bytes read by the job. - -`input_field_count`:: - (long) The total number of record fields read by the job. This count includes - fields that are not used in the analysis. - -`input_record_count`:: - (long) The number of data records read by the job. - -`invalid_date_count`:: - (long) The number of records with either a missing date field or a date that could not be parsed. - -`job_id`:: - (string) A unique identifier for the job. - -`last_data_time`:: - (date) The timestamp at which data was last analyzed, according to server time. - -`latest_empty_bucket_timestamp`:: - (date) The timestamp of the last bucket that did not contain any data. - -`latest_record_timestamp`:: - (date) The timestamp of the latest chronologically input document. - -`latest_sparse_bucket_timestamp`:: - (date) The timestamp of the last bucket that was considered sparse. - -`missing_field_count`:: - (long) The number of records that are missing a field that the job is - configured to analyze. Records with missing fields are still processed because - it is possible that not all fields are missing. The value of - `processed_record_count` includes this count. + - -NOTE: If you are using {dfeeds} or posting data to the job in JSON format, a -high `missing_field_count` is often not an indication of data issues. It is not -necessarily a cause for concern. - -`out_of_order_timestamp_count`:: - (long) The number of records that are out of time sequence and - outside of the latency window. This information is applicable only when - you provide data to the job by using the <>. - These out of order records are discarded, since jobs require time series data - to be in ascending chronological order. - -`processed_field_count`:: - (long) The total number of fields in all the records that have been processed - by the job. Only fields that are specified in the detector configuration - object contribute to this count. The time stamp is not included in this count. - -`processed_record_count`:: - (long) The number of records that have been processed by the job. - This value includes records with missing fields, since they are nonetheless - analyzed. + - If you use {dfeeds} and have aggregations in your search query, - the `processed_record_count` will be the number of aggregated records - processed, not the number of {es} documents. - -`sparse_bucket_count`:: - (long) The number of buckets that contained few data points compared to the - expected number of data points. If your data contains many sparse buckets, - consider using a longer `bucket_span`. - -[float] -[[ml-modelsizestats]] -==== Model Size Stats Objects - -The `model_size_stats` object has the following properties: - -`bucket_allocation_failures_count`:: - (long) The number of buckets for which new entities in incoming data were not - processed due to insufficient model memory. This situation is also signified - by a `hard_limit: memory_status` property value. - -`job_id`:: - (string) A numerical character string that uniquely identifies the job. - -`log_time`:: - (date) The timestamp of the `model_size_stats` according to server time. - -`memory_status`:: - (string) The status of the mathematical models. - This property can have one of the following values: - `ok`::: The models stayed below the configured value. - `soft_limit`::: The models used more than 60% of the configured memory limit - and older unused models will be pruned to free up space. - `hard_limit`::: The models used more space than the configured memory limit. - As a result, not all incoming data was processed. - -`model_bytes`:: - (long) The number of bytes of memory used by the models. This is the maximum - value since the last time the model was persisted. If the job is closed, - this value indicates the latest size. - -`result_type`:: - (string) For internal use. The type of result. - -`total_by_field_count`:: - (long) The number of `by` field values that were analyzed by the models.+ - -NOTE: The `by` field values are counted separately for each detector and partition. - -`total_over_field_count`:: - (long) The number of `over` field values that were analyzed by the models.+ - -NOTE: The `over` field values are counted separately for each detector and partition. - -`total_partition_field_count`:: - (long) The number of `partition` field values that were analyzed by the models. - -`timestamp`:: - (date) The timestamp of the `model_size_stats` according to the timestamp of the data. - -[float] -[[ml-forecastsstats]] -==== Forecasts Stats Objects - -The `forecasts_stats` object shows statistics about forecasts. It has the following properties: - -`total`:: - (long) The number of forecasts currently available for this model. - -`forecasted_jobs`:: - (long) The number of jobs that have at least one forecast. - -`memory_bytes`:: - (object) Statistics about the memory usage: minimum, maximum, average and total. - -`records`:: - (object) Statistics about the number of forecast records: minimum, maximum, average and total. - -`processing_time_ms`:: - (object) Statistics about the forecast runtime in milliseconds: minimum, maximum, average and total. - -`status`:: - (object) Counts per forecast status, for example: {"finished" : 2}. - -NOTE: `memory_bytes`, `records`, `processing_time_ms` and `status` require at least 1 forecast, otherwise -these fields are omitted. - -[float] -[[ml-timingstats]] -==== Timing Stats Objects - -The `timing_stats` object shows timing-related statistics about the job's progress. It has the following properties: - -`job_id`:: - (string) A numerical character string that uniquely identifies the job. - -`bucket_count`:: - (long) The number of buckets processed. - -`minimum_bucket_processing_time_ms`:: - (double) Minimum among all bucket processing times in milliseconds. - -`maximum_bucket_processing_time_ms`:: - (double) Maximum among all bucket processing times in milliseconds. - -`average_bucket_processing_time_ms`:: - (double) Average of all bucket processing times in milliseconds. - -`exponential_average_bucket_processing_time_ms`:: - (double) Exponential moving average of all bucket processing times in milliseconds. - - -[float] -[[ml-stats-node]] -==== Node Objects - -The `node` objects contains properties for the node that runs the job. -This information is available only for open jobs. - -`id`:: - (string) The unique identifier of the node. - -`name`:: - (string) The node name. - -`ephemeral_id`:: - (string) The ephemeral id of the node. - -`transport_address`:: - (string) The host and port where transport HTTP connections are accepted. - -`attributes`:: - (object) For example, {"ml.machine_memory": "17179869184"}. diff --git a/docs/reference/ml/anomaly-detection/apis/post-data.asciidoc b/docs/reference/ml/anomaly-detection/apis/post-data.asciidoc index cfd3d4ca67fdc..d4fa82b217f64 100644 --- a/docs/reference/ml/anomaly-detection/apis/post-data.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/post-data.asciidoc @@ -37,10 +37,10 @@ and upload each one separately in sequential time order. When running in real time, it is generally recommended that you perform many small uploads, rather than queueing data to upload larger files. -When uploading data, check the <> for progress. -The following records will not be processed: +When uploading data, check the job data counts for progress. +The following documents will not be processed: -* Records not in chronological order and outside the latency window +* Documents not in chronological order and outside the latency window * Records with an invalid timestamp //TBD link to Working with Out of Order timeseries concept doc @@ -109,4 +109,4 @@ the job. For example: } ---- -For more information about these properties, see <>. +For more information about these properties, see <>. diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index f277e6ab2e4ad..65fd370dda9ad 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -576,11 +576,6 @@ tag::job-id-datafeed[] The unique identifier for the job to which the {dfeed} sends data. end::job-id-datafeed[] -tag::jobs-stats-anomaly-detection[] -An array of {anomaly-job} statistics objects. -For more information, see <>. -end::jobs-stats-anomaly-detection[] - tag::lambda[] `lambda`:: (Optional, double) Regularization parameter to prevent overfitting on the diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index afc10a4493b17..858e128944742 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -1065,4 +1065,20 @@ See the details in <>, <>, [[ml-datafeed-delayed-data-check-config]] <>, [[ml-datafeed-counts]] -<>. \ No newline at end of file +<>. + +[role="exclude",id="ml-jobstats"] +=== Job statistics + +This +[[ml-datacounts]] +page +[[ml-modelsizestats]] +was +[[ml-forecastsstats]] +deleted. +[[ml-timingstats]] +See +[[ml-stats-node]] +the details in <>. + diff --git a/docs/reference/rest-api/defs.asciidoc b/docs/reference/rest-api/defs.asciidoc index 8bdf35e62f11b..0ef152e917f8e 100644 --- a/docs/reference/rest-api/defs.asciidoc +++ b/docs/reference/rest-api/defs.asciidoc @@ -7,7 +7,6 @@ These resource definitions are used in APIs related to {ml-features} and * <> * <> -* <> * <> * <> * <> @@ -15,7 +14,6 @@ These resource definitions are used in APIs related to {ml-features} and include::{es-repo-dir}/ml/df-analytics/apis/dfanalyticsresources.asciidoc[] include::{es-repo-dir}/ml/df-analytics/apis/evaluateresources.asciidoc[] -include::{es-repo-dir}/ml/anomaly-detection/apis/jobcounts.asciidoc[] include::{es-repo-dir}/ml/anomaly-detection/apis/snapshotresource.asciidoc[] include::{xes-repo-dir}/rest-api/security/role-mapping-resources.asciidoc[] include::{es-repo-dir}/ml/anomaly-detection/apis/resultsresource.asciidoc[] From 2a5e0752d53d505284ff416a511c8991f95b1456 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Wed, 11 Dec 2019 13:01:54 -0800 Subject: [PATCH 158/686] Fall back to Java 13 APIs for forbidden API checks when using JDK 14 (#50095) Closes #50041 --- .../gradle/precommit/PrecommitTasks.groovy | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy index 8bc47407b1754..52f98330ee50d 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy @@ -24,6 +24,7 @@ import de.thetaphi.forbiddenapis.gradle.ForbiddenApisPlugin import org.elasticsearch.gradle.ExportElasticsearchBuildResourcesTask import org.elasticsearch.gradle.VersionProperties import org.elasticsearch.gradle.info.BuildParams +import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.plugins.JavaBasePlugin @@ -145,16 +146,13 @@ class PrecommitTasks { doFirst { // we need to defer this configuration since we don't know the runtime java version until execution time targetCompatibility = BuildParams.runtimeJavaVersion.majorVersion - /* - TODO: Reenable once Gradle supports Java 13 or later! if (BuildParams.runtimeJavaVersion > JavaVersion.VERSION_13) { - project.logger.info( - "Forbidden APIs does not support java version past 13. Will use the signatures from 13 for ", - BuildParams.runtimeJavaVersion` + project.logger.warn( + "Forbidden APIs does not support Java versions past 13. Will use the signatures from 13 for {}.", + BuildParams.runtimeJavaVersion ) - targetCompatibility = JavaVersion.VERSION_13.getMajorVersion() + targetCompatibility = JavaVersion.VERSION_13.majorVersion } - */ } bundledSignatures = [ "jdk-unsafe", "jdk-deprecated", "jdk-non-portable", "jdk-system-out" From f32d05db250efb6d45859d84cc06af9727589fce Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 11 Dec 2019 16:02:31 -0500 Subject: [PATCH 159/686] Require JDK 13 for compilation (#50004) We have a long history of advancing the required compiler to the newest JDK. JDK 13 has been with us for awhile, but we were blocked from upgrading since Gradle was not compatible with JDK 13. With the advancement in our project to Gradle 6 which supports JDK 13, we can now advance our minimum compiler version. This commit updates the minimum compiler version to JDK 13. --- .ci/build.sh | 2 +- .ci/java-versions.properties | 2 +- .ci/matrix-build-javas.yml | 2 +- .ci/matrix-runtime-javas.yml | 3 --- .ci/packer_cache.sh | 1 + CONTRIBUTING.md | 8 ++++---- .../org/elasticsearch/gradle/test/DistroTestPlugin.java | 2 +- buildSrc/src/main/resources/minimumCompilerVersion | 2 +- 8 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.ci/build.sh b/.ci/build.sh index 6dfc06a0912fb..b86acfd8cef1d 100755 --- a/.ci/build.sh +++ b/.ci/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -JAVA_HOME=${JAVA_HOME:-$HOME/.java/openjdk12} +JAVA_HOME=${JAVA_HOME:-$HOME/.java/openjdk13} RUNTIME_JAVA_HOME=${RUNTIME_JAVA_HOME:-$HOME/.java/openjdk11} JAVA7_HOME=$HOME/.java/java7 diff --git a/.ci/java-versions.properties b/.ci/java-versions.properties index 1c1e813553670..f5c4978113ba3 100644 --- a/.ci/java-versions.properties +++ b/.ci/java-versions.properties @@ -4,7 +4,7 @@ # build and test Elasticsearch for this branch. Valid Java versions # are 'java' or 'openjdk' followed by the major release number. -ES_BUILD_JAVA=openjdk12 +ES_BUILD_JAVA=openjdk13 ES_RUNTIME_JAVA=openjdk11 GRADLE_TASK=build diff --git a/.ci/matrix-build-javas.yml b/.ci/matrix-build-javas.yml index 202fd60edea4c..49b904f2c32ac 100644 --- a/.ci/matrix-build-javas.yml +++ b/.ci/matrix-build-javas.yml @@ -6,4 +6,4 @@ # or 'openjdk' followed by the major release number. ES_BUILD_JAVA: - - openjdk12 + - openjdk13 diff --git a/.ci/matrix-runtime-javas.yml b/.ci/matrix-runtime-javas.yml index 29e3c95004fdc..9e2422d950931 100644 --- a/.ci/matrix-runtime-javas.yml +++ b/.ci/matrix-runtime-javas.yml @@ -7,11 +7,8 @@ ES_RUNTIME_JAVA: - java11 - - java12 - - openjdk12 - openjdk13 - openjdk14 - zulu11 - - zulu12 - corretto11 - adoptopenjdk11 diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh index adc4f80d4960d..759a3aae9aeb6 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -21,5 +21,6 @@ export JAVA_HOME="${HOME}"/.java/${ES_BUILD_JAVA} export JAVA8_HOME="${HOME}"/.java/java8 export JAVA11_HOME="${HOME}"/.java/java11 export JAVA12_HOME="${HOME}"/.java/openjdk12 +export JAVA13_HOME="${HOME}"/.java/openjdk13 ./gradlew --parallel clean --scan -Porg.elasticsearch.acceptScanTOS=true -s resolveAllDependencies diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53935ec15feb4..ed96a2449568a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,16 +92,16 @@ Contributing to the Elasticsearch codebase **Repository:** [https://github.com/elastic/elasticsearch](https://github.com/elastic/elasticsearch) -JDK 12 is required to build Elasticsearch. You must have a JDK 12 installation +JDK 13 is required to build Elasticsearch. You must have a JDK 13 installation with the environment variable `JAVA_HOME` referencing the path to Java home for -your JDK 12 installation. By default, tests use the same runtime as `JAVA_HOME`. +your JDK 13 installation. By default, tests use the same runtime as `JAVA_HOME`. However, since Elasticsearch supports JDK 11, the build supports compiling with -JDK 12 and testing on a JDK 11 runtime; to do this, set `RUNTIME_JAVA_HOME` +JDK 13 and testing on a JDK 11 runtime; to do this, set `RUNTIME_JAVA_HOME` pointing to the Java home of a JDK 11 installation. Note that this mechanism can be used to test against other JDKs as well, this is not only limited to JDK 11. > Note: It is also required to have `JAVA8_HOME`, `JAVA9_HOME`, `JAVA10_HOME` -and `JAVA11_HOME` available so that the tests can pass. +and `JAVA11_HOME`, and `JAVA12_HOME` available so that the tests can pass. > Warning: do not use `sdkman` for Java installations which do not have proper `jrunscript` for jdk distributions. diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/DistroTestPlugin.java b/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/DistroTestPlugin.java index 45a1fc0266b65..e8294ce738b7f 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/DistroTestPlugin.java +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/DistroTestPlugin.java @@ -75,7 +75,7 @@ public class DistroTestPlugin implements Plugin { private static final String SYSTEM_JDK_VERSION = "11.0.2+9"; private static final String SYSTEM_JDK_VENDOR = "openjdk"; - private static final String GRADLE_JDK_VERSION = "12.0.1+12@69cfe15208a647278a19ef0990eea691"; + private static final String GRADLE_JDK_VERSION = "13.0.1+9@cec27d702aa74d5a8630c65ae61e4305"; private static final String GRADLE_JDK_VENDOR = "openjdk"; // all distributions used by distro tests. this is temporary until tests are per distribution diff --git a/buildSrc/src/main/resources/minimumCompilerVersion b/buildSrc/src/main/resources/minimumCompilerVersion index 3cacc0b93c9c9..b1bd38b62a080 100644 --- a/buildSrc/src/main/resources/minimumCompilerVersion +++ b/buildSrc/src/main/resources/minimumCompilerVersion @@ -1 +1 @@ -12 \ No newline at end of file +13 From 034aafd59c381ab55e1473818fe925059fbbf042 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Wed, 11 Dec 2019 22:12:56 +0100 Subject: [PATCH 160/686] Address UUIDTests#testCompression failures. (#50093) Those were due to codec randomization. Closes #50048 --- .../src/test/java/org/elasticsearch/common/UUIDTests.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/common/UUIDTests.java b/server/src/test/java/org/elasticsearch/common/UUIDTests.java index 1d23570064fe5..5f7dea14946e6 100644 --- a/server/src/test/java/org/elasticsearch/common/UUIDTests.java +++ b/server/src/test/java/org/elasticsearch/common/UUIDTests.java @@ -22,6 +22,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.lucene.codecs.Codec; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field.Store; import org.apache.lucene.document.StringField; @@ -29,6 +30,8 @@ import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.SerialMergeScheduler; import org.apache.lucene.store.Directory; +import org.apache.lucene.util.TestUtil; +import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; @@ -116,7 +119,6 @@ public void testUUIDThreaded(UUIDGenerator uuidSource) { assertEquals(count*uuids, globalSet.size()); } - @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/50048") public void testCompression() throws Exception { Logger logger = LogManager.getLogger(UUIDTests.class); // Low number so that the test runs quickly, but the results are more interesting with larger numbers @@ -135,7 +137,7 @@ private static double testCompression(int numDocs, int numDocsPerSecond, int num random().nextBytes(macAddresses[i]); } UUIDGenerator generator = new TimeBasedUUIDGenerator() { - double currentTimeMillis = System.currentTimeMillis(); + double currentTimeMillis = TestUtil.nextLong(random(), 0L, 10000000000L); @Override protected long currentTimeMillis() { @@ -152,6 +154,7 @@ protected byte[] macAddress() { // the quality of this test Directory dir = newFSDirectory(createTempDir()); IndexWriterConfig config = new IndexWriterConfig() + .setCodec(Codec.forName(Lucene.LATEST_CODEC)) .setMergeScheduler(new SerialMergeScheduler()); // for reproducibility IndexWriter w = new IndexWriter(dir, config); Document doc = new Document(); From e283e71f57ab06799805dfe61f0222291a03b674 Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Wed, 11 Dec 2019 23:53:03 +0200 Subject: [PATCH 161/686] Have COUNT DISTINCT return 0 instead of NULL for no documents matching. (#50037) --- .../sql/qa/src/main/resources/agg.csv-spec | 23 +++++++++++++++++++ .../search/extractor/MetricAggExtractor.java | 4 ---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec index ed1ae60b14c85..2c1e1126ee1da 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec @@ -657,6 +657,29 @@ SELECT COUNT(first_name)=COUNT(DISTINCT first_name) AS areEqual, COUNT(first_nam true |90 |90 ; +aggCountWithNull +schema::COUNT(NULL):l|COUNT(*):l|COUNT(DISTINCT languages):l|languages:bt +SELECT COUNT(NULL), COUNT(*), COUNT(DISTINCT languages), languages FROM test_emp GROUP BY languages ORDER BY languages DESC; + + COUNT(NULL) | COUNT(*) |COUNT(DISTINCT languages)| languages +---------------+---------------+-------------------------+--------------- +null |21 |1 |5 +null |18 |1 |4 +null |17 |1 |3 +null |19 |1 |2 +null |15 |1 |1 +null |10 |0 |null +; + +aggCountZeroDocuments +schema::COUNT(NULL):l|COUNT(*):l|COUNT(DISTINCT languages):l +SELECT COUNT(NULL), COUNT(*), COUNT(DISTINCT languages) FROM test_emp WHERE languages > 100; + + COUNT(NULL) | COUNT(*) |COUNT(DISTINCT languages) +---------------+---------------+------------------------- +null |0 |0 +; + aggCountAllEquality schema::areEqual:b|afn:l SELECT COUNT(first_name)=COUNT(ALL first_name) AS areEqual, COUNT(ALL first_name) afn FROM test_emp; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/MetricAggExtractor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/MetricAggExtractor.java index 07309c3582200..007c7a26c4966 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/MetricAggExtractor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/MetricAggExtractor.java @@ -12,7 +12,6 @@ import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter; import org.elasticsearch.search.aggregations.matrix.stats.InternalMatrixStats; import org.elasticsearch.search.aggregations.metrics.InternalAvg; -import org.elasticsearch.search.aggregations.metrics.InternalCardinality; import org.elasticsearch.search.aggregations.metrics.InternalMax; import org.elasticsearch.search.aggregations.metrics.InternalMin; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; @@ -149,9 +148,6 @@ private static boolean containsValues(InternalAggregation agg) { if (agg instanceof InternalAvg) { return hasValue((InternalAvg) agg); } - if (agg instanceof InternalCardinality) { - return hasValue((InternalCardinality) agg); - } if (agg instanceof InternalSum) { return hasValue((InternalSum) agg); } From ba16c070ab77fa893aeffd92fae5e9e0ee3f71c5 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Wed, 11 Dec 2019 16:24:31 -0700 Subject: [PATCH 162/686] Update TcpHeader version constant for backport (#50086) In 7.6 we are adding a int indicating how long the Elasticsearch variable header is. This commit modifies the version constant indicating when this int is supported to reflect the fact that this int is available on 7.6. --- .../java/org/elasticsearch/transport/TcpHeader.java | 5 ++--- .../transport/AbstractSimpleTransportTestCase.java | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/TcpHeader.java b/server/src/main/java/org/elasticsearch/transport/TcpHeader.java index dfa79d240e470..10feeec5ec550 100644 --- a/server/src/main/java/org/elasticsearch/transport/TcpHeader.java +++ b/server/src/main/java/org/elasticsearch/transport/TcpHeader.java @@ -25,9 +25,8 @@ import java.io.IOException; public class TcpHeader { - - // TODO: Change to 7.6 after backport - public static final Version VERSION_WITH_HEADER_SIZE = Version.V_8_0_0; + + public static final Version VERSION_WITH_HEADER_SIZE = Version.V_7_6_0; public static final int MARKER_BYTES_SIZE = 2; diff --git a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java index 4dddb0af9b4c4..109a725672098 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java @@ -2368,15 +2368,15 @@ public String executor() { assertEquals(1, transportStats.getRxCount()); assertEquals(2, transportStats.getTxCount()); assertEquals(138, transportStats.getRxSize().getBytes()); - assertEquals(107, transportStats.getTxSize().getBytes()); + assertEquals(111, transportStats.getTxSize().getBytes()); }); sendResponseLatch.countDown(); responseLatch.await(); stats = serviceC.transport.getStats(); // response has been received assertEquals(2, stats.getRxCount()); assertEquals(2, stats.getTxCount()); - assertEquals(159, stats.getRxSize().getBytes()); - assertEquals(107, stats.getTxSize().getBytes()); + assertEquals(163, stats.getRxSize().getBytes()); + assertEquals(111, stats.getTxSize().getBytes()); } finally { serviceC.close(); } @@ -2483,7 +2483,7 @@ public String executor() { assertEquals(1, transportStats.getRxCount()); assertEquals(2, transportStats.getTxCount()); assertEquals(138, transportStats.getRxSize().getBytes()); - assertEquals(107, transportStats.getTxSize().getBytes()); + assertEquals(111, transportStats.getTxSize().getBytes()); }); sendResponseLatch.countDown(); responseLatch.await(); @@ -2497,8 +2497,8 @@ public String executor() { String failedMessage = "Unexpected read bytes size. The transport exception that was received=" + exception; // 49 bytes are the non-exception message bytes that have been received. It should include the initial // handshake message and the header, version, etc bytes in the exception message. - assertEquals(failedMessage, 162 + streamOutput.bytes().length(), stats.getRxSize().getBytes()); - assertEquals(107, stats.getTxSize().getBytes()); + assertEquals(failedMessage, 166 + streamOutput.bytes().length(), stats.getRxSize().getBytes()); + assertEquals(111, stats.getTxSize().getBytes()); } finally { serviceC.close(); } From c440f782e385caf68be6d1768d67be51e0946609 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Wed, 11 Dec 2019 15:39:06 -0800 Subject: [PATCH 163/686] Allow individual projects to override test heapdump setting (#50096) This commit goes from using a JvmArgumentProvider to using the normal Test task APIs for passing the `HeapDumpOnOutOfMemoryError` JVM argument. This makes it simpler for subprojects, such as lang-painless to override this setting if necessary. Closes #49117 --- .../main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index 05ea77720e123..2f7c3af5da3d7 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -672,9 +672,10 @@ class BuildPlugin implements Plugin { test.jvmArgs "-Xmx${System.getProperty('tests.heap.size', '512m')}", "-Xms${System.getProperty('tests.heap.size', '512m')}", - '--illegal-access=warn' + '--illegal-access=warn', + '-XX:+HeapDumpOnOutOfMemoryError' - test.jvmArgumentProviders.add({ ['-XX:+HeapDumpOnOutOfMemoryError', "-XX:HeapDumpPath=$heapdumpDir"] } as CommandLineArgumentProvider) + test.jvmArgumentProviders.add({ ["-XX:HeapDumpPath=$heapdumpDir"] } as CommandLineArgumentProvider) if (System.getProperty('tests.jvm.argline')) { test.jvmArgs System.getProperty('tests.jvm.argline').split(" ") From 235f336fffc379282994f7749aea0d72fe7a886b Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Wed, 11 Dec 2019 16:08:47 -0800 Subject: [PATCH 164/686] Simplify running tools in packaging tests (#49665) Running tools requires a shell. This should be the shell setup by the base packaging tests, but currently tests must pass in their own shell. This commit begins to make running tools easier by eliminating the shell argument, instead keeping the shell as part of the Installation (which can eventually be passed through from the test itself on installation). The variable names for each tool are also simplified. --- .../packaging/test/ArchiveTests.java | 37 ++++++++-------- .../packaging/test/DebPreservationTests.java | 4 +- .../packaging/test/DockerTests.java | 16 +++---- .../packaging/test/PackageTests.java | 3 +- .../packaging/test/PackagingTestCase.java | 42 +++++++++---------- .../packaging/test/PasswordToolsTests.java | 6 +-- .../packaging/test/RpmPreservationTests.java | 10 ++--- .../packaging/test/SqlCliTests.java | 2 +- .../packaging/test/WindowsServiceTests.java | 2 +- .../packaging/util/Archives.java | 10 ++--- .../elasticsearch/packaging/util/Docker.java | 2 +- .../packaging/util/Installation.java | 37 +++++++++------- .../packaging/util/Packages.java | 5 +-- .../elasticsearch/packaging/util/Shell.java | 9 ++++ 14 files changed, 95 insertions(+), 90 deletions(-) diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java index d427124d7fecd..9691636f5ccda 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java @@ -25,7 +25,6 @@ import org.elasticsearch.packaging.util.Installation; import org.elasticsearch.packaging.util.Platforms; import org.elasticsearch.packaging.util.ServerUtils; -import org.elasticsearch.packaging.util.Shell; import org.elasticsearch.packaging.util.Shell.Result; import org.junit.BeforeClass; @@ -63,13 +62,13 @@ public static void filterDistros() { } public void test10Install() throws Exception { - installation = installArchive(distribution()); + installation = installArchive(sh, distribution()); verifyArchiveInstallation(installation, distribution()); } public void test20PluginsListWithNoPlugins() throws Exception { final Installation.Executables bin = installation.executables(); - final Result r = bin.elasticsearchPlugin.run(sh, "list"); + final Result r = bin.pluginTool.run("list"); assertThat(r.stdout, isEmptyString()); } @@ -109,26 +108,26 @@ public void test31BadJavaHome() throws Exception { public void test40CreateKeystoreManually() throws Exception { final Installation.Executables bin = installation.executables(); - Platforms.onLinux(() -> sh.run("sudo -u " + ARCHIVE_OWNER + " " + bin.elasticsearchKeystore + " create")); + Platforms.onLinux(() -> sh.run("sudo -u " + ARCHIVE_OWNER + " " + bin.keystoreTool + " create")); // this is a hack around the fact that we can't run a command in the same session as the same user but not as administrator. // the keystore ends up being owned by the Administrators group, so we manually set it to be owned by the vagrant user here. // from the server's perspective the permissions aren't really different, this is just to reflect what we'd expect in the tests. // when we run these commands as a role user we won't have to do this Platforms.onWindows(() -> { - sh.run(bin.elasticsearchKeystore + " create"); + sh.run(bin.keystoreTool + " create"); sh.chown(installation.config("elasticsearch.keystore")); }); assertThat(installation.config("elasticsearch.keystore"), file(File, ARCHIVE_OWNER, ARCHIVE_OWNER, p660)); Platforms.onLinux(() -> { - final Result r = sh.run("sudo -u " + ARCHIVE_OWNER + " " + bin.elasticsearchKeystore + " list"); + final Result r = sh.run("sudo -u " + ARCHIVE_OWNER + " " + bin.keystoreTool + " list"); assertThat(r.stdout, containsString("keystore.seed")); }); Platforms.onWindows(() -> { - final Result r = sh.run(bin.elasticsearchKeystore + " list"); + final Result r = sh.run(bin.keystoreTool + " list"); assertThat(r.stdout, containsString("keystore.seed")); }); } @@ -202,7 +201,6 @@ public void test52BundledJdkRemoved() throws Exception { public void test53JavaHomeWithSpecialCharacters() throws Exception { Platforms.onWindows(() -> { - final Shell sh = new Shell(); String javaPath = "C:\\Program Files (x86)\\java"; try { // once windows 2012 is no longer supported and powershell 5.0 is always available we can change this command @@ -228,7 +226,6 @@ public void test53JavaHomeWithSpecialCharacters() throws Exception { }); Platforms.onLinux(() -> { - final Shell sh = newShell(); // Create temporary directory with a space and link to real java home String testJavaHome = Paths.get("/tmp", "java home").toString(); try { @@ -256,12 +253,12 @@ public void test60AutoCreateKeystore() throws Exception { final Installation.Executables bin = installation.executables(); Platforms.onLinux(() -> { - final Result result = sh.run("sudo -u " + ARCHIVE_OWNER + " " + bin.elasticsearchKeystore + " list"); + final Result result = sh.run("sudo -u " + ARCHIVE_OWNER + " " + bin.keystoreTool + " list"); assertThat(result.stdout, containsString("keystore.seed")); }); Platforms.onWindows(() -> { - final Result result = sh.run(bin.elasticsearchKeystore + " list"); + final Result result = sh.run(bin.keystoreTool + " list"); assertThat(result.stdout, containsString("keystore.seed")); }); } @@ -339,11 +336,11 @@ public void test90SecurityCliPackaging() throws Exception { if (distribution().isDefault()) { assertTrue(Files.exists(installation.lib.resolve("tools").resolve("security-cli"))); final Platforms.PlatformAction action = () -> { - Result result = sh.run(bin.elasticsearchCertutil + " --help"); + Result result = sh.run(bin.certutilTool + " --help"); assertThat(result.stdout, containsString("Simplifies certificate creation for use with the Elastic Stack")); // Ensure that the exit code from the java command is passed back up through the shell script - result = sh.runIgnoreExitCode(bin.elasticsearchCertutil + " invalid-command"); + result = sh.runIgnoreExitCode(bin.certutilTool + " invalid-command"); assertThat(result.exitCode, is(not(0))); assertThat(result.stderr, containsString("Unknown command [invalid-command]")); }; @@ -358,7 +355,7 @@ public void test91ElasticsearchShardCliPackaging() throws Exception { final Installation.Executables bin = installation.executables(); Platforms.PlatformAction action = () -> { - final Result result = sh.run(bin.elasticsearchShard + " -h"); + final Result result = sh.run(bin.shardTool + " -h"); assertThat(result.stdout, containsString("A CLI tool to remove corrupted parts of unrecoverable shards")); }; @@ -373,7 +370,7 @@ public void test92ElasticsearchNodeCliPackaging() throws Exception { final Installation.Executables bin = installation.executables(); Platforms.PlatformAction action = () -> { - final Result result = sh.run(bin.elasticsearchNode + " -h"); + final Result result = sh.run(bin.nodeTool + " -h"); assertThat(result.stdout, containsString("A CLI tool to do unsafe cluster and index manipulations on current node")); }; @@ -394,7 +391,7 @@ public void test93ElasticsearchNodeCustomDataPathAndNotEsHomeWorkDir() throws Ex startElasticsearch(); Archives.stopElasticsearch(installation); - Result result = sh.run("echo y | " + installation.executables().elasticsearchNode + " unsafe-bootstrap"); + Result result = sh.run("echo y | " + installation.executables().nodeTool + " unsafe-bootstrap"); assertThat(result.stdout, containsString("Master node was successfully bootstrapped")); } @@ -404,16 +401,16 @@ public void test94ElasticsearchNodeExecuteCliNotEsHomeWorkDir() throws Exception sh.setWorkingDirectory(getTempDir()); Platforms.PlatformAction action = () -> { - Result result = sh.run(bin.elasticsearchCertutil+ " -h"); + Result result = sh.run(bin.certutilTool + " -h"); assertThat(result.stdout, containsString("Simplifies certificate creation for use with the Elastic Stack")); - result = sh.run(bin.elasticsearchSyskeygen+ " -h"); + result = sh.run(bin.syskeygenTool + " -h"); assertThat(result.stdout, containsString("system key tool")); - result = sh.run(bin.elasticsearchSetupPasswords+ " -h"); + result = sh.run(bin.setupPasswordsTool + " -h"); assertThat(result.stdout, containsString("Sets the passwords for reserved users")); - result = sh.run(bin.elasticsearchUsers+ " -h"); + result = sh.run(bin.usersTool + " -h"); assertThat(result.stdout, containsString("Manages elasticsearch file users")); }; diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DebPreservationTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DebPreservationTests.java index ea4f5565a98a8..4ff08ad47acf6 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DebPreservationTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DebPreservationTests.java @@ -47,9 +47,9 @@ public static void filterDistros() { public void test10Install() throws Exception { assertRemoved(distribution()); - installation = installPackage(distribution()); + installation = installPackage(sh, distribution()); assertInstalled(distribution()); - verifyPackageInstallation(installation, distribution(), newShell()); + verifyPackageInstallation(installation, distribution(), sh); } public void test20Remove() throws Exception { diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java index 163199d833ae1..c8575902721c0 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java @@ -130,7 +130,7 @@ public void test011PresenceOfXpack() throws Exception { */ public void test020PluginsListWithNoPlugins() { final Installation.Executables bin = installation.executables(); - final Result r = sh.run(bin.elasticsearchPlugin + " list"); + final Result r = sh.run(bin.pluginTool + " list"); assertThat("Expected no plugins to be listed", r.stdout, emptyString()); } @@ -148,9 +148,9 @@ public void test040CreateKeystoreManually() throws InterruptedException { // Move the auto-created one out of the way, or else the CLI prompts asks us to confirm sh.run("mv " + keystorePath + " " + keystorePath + ".bak"); - sh.run(bin.elasticsearchKeystore + " create"); + sh.run(bin.keystoreTool + " create"); - final Result r = sh.run(bin.elasticsearchKeystore + " list"); + final Result r = sh.run(bin.keystoreTool + " list"); assertThat(r.stdout, containsString("keystore.seed")); } @@ -165,7 +165,7 @@ public void test041AutoCreateKeystore() throws Exception { assertPermissionsAndOwnership(keystorePath, p660); final Installation.Executables bin = installation.executables(); - final Result result = sh.run(bin.elasticsearchKeystore + " list"); + final Result result = sh.run(bin.keystoreTool + " list"); assertThat(result.stdout, containsString("keystore.seed")); } @@ -402,11 +402,11 @@ public void test090SecurityCliPackaging() { if (distribution().isDefault()) { assertTrue(existsInContainer(securityCli)); - Result result = sh.run(bin.elasticsearchCertutil + " --help"); + Result result = sh.run(bin.certutilTool + " --help"); assertThat(result.stdout, containsString("Simplifies certificate creation for use with the Elastic Stack")); // Ensure that the exit code from the java command is passed back up through the shell script - result = sh.runIgnoreExitCode(bin.elasticsearchCertutil + " invalid-command"); + result = sh.runIgnoreExitCode(bin.certutilTool + " invalid-command"); assertThat(result.isSuccess(), is(false)); assertThat(result.stdout, containsString("Unknown command [invalid-command]")); } else { @@ -420,7 +420,7 @@ public void test090SecurityCliPackaging() { public void test091ElasticsearchShardCliPackaging() { final Installation.Executables bin = installation.executables(); - final Result result = sh.run(bin.elasticsearchShard + " -h"); + final Result result = sh.run(bin.shardTool + " -h"); assertThat(result.stdout, containsString("A CLI tool to remove corrupted parts of unrecoverable shards")); } @@ -430,7 +430,7 @@ public void test091ElasticsearchShardCliPackaging() { public void test092ElasticsearchNodeCliPackaging() { final Installation.Executables bin = installation.executables(); - final Result result = sh.run(bin.elasticsearchNode + " -h"); + final Result result = sh.run(bin.nodeTool + " -h"); assertThat( "Failed to find expected message about the elasticsearch-node CLI tool", result.stdout, diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java index c992b02b1c522..b53878459ef6d 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java @@ -72,7 +72,7 @@ public static void filterDistros() { public void test10InstallPackage() throws Exception { assertRemoved(distribution()); - installation = installPackage(distribution()); + installation = installPackage(sh, distribution()); assertInstalled(distribution()); verifyPackageInstallation(installation, distribution(), sh); } @@ -302,7 +302,6 @@ public void test82SystemdMask() throws Exception { assumeTrue(isSystemd()); sh.run("systemctl mask systemd-sysctl.service"); - install(); sh.run("systemctl unmask systemd-sysctl.service"); diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java index f797d8d49684a..da3f55daae035 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java @@ -37,7 +37,6 @@ import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.ClassRule; import org.junit.Rule; import org.junit.rules.TestName; import org.junit.rules.TestWatcher; @@ -89,8 +88,8 @@ public abstract class PackagingTestCase extends Assert { private static boolean failed; - @ClassRule - public static final TestWatcher testFailureRule = new TestWatcher() { + @Rule + public final TestWatcher testFailureRule = new TestWatcher() { @Override protected void failed(Throwable e, Description description) { failed = true; @@ -98,7 +97,7 @@ protected void failed(Throwable e, Description description) { }; // a shell to run system commands with - protected Shell sh; + protected static Shell sh; @Rule public final TestName testNameRule = new TestName(); @@ -114,11 +113,24 @@ public static void cleanup() throws Exception { cleanEverything(); } + @BeforeClass + public static void createShell() throws Exception { + sh = new Shell(); + } + @Before public void setup() throws Exception { assumeFalse(failed); // skip rest of tests once one fails - sh = newShell(); + sh.reset(); + if (distribution().hasJdk == false) { + Platforms.onLinux(() -> { + sh.getEnv().put("JAVA_HOME", systemJavaHome); + }); + Platforms.onWindows(() -> { + sh.getEnv().put("JAVA_HOME", systemJavaHome); + }); + } } /** The {@link Distribution} that should be tested in this case */ @@ -130,13 +142,13 @@ protected static void install() throws Exception { switch (distribution.packaging) { case TAR: case ZIP: - installation = Archives.installArchive(distribution); + installation = Archives.installArchive(sh, distribution); Archives.verifyArchiveInstallation(installation, distribution); break; case DEB: case RPM: - installation = Packages.installPackage(distribution); - Packages.verifyPackageInstallation(installation, distribution, newShell()); + installation = Packages.installPackage(sh, distribution); + Packages.verifyPackageInstallation(installation, distribution, sh); break; case DOCKER: installation = Docker.runContainer(distribution); @@ -176,19 +188,6 @@ protected void assertWhileRunning(Platforms.PlatformAction assertions) throws Ex stopElasticsearch(); } - protected static Shell newShell() throws Exception { - Shell sh = new Shell(); - if (distribution().hasJdk == false) { - Platforms.onLinux(() -> { - sh.getEnv().put("JAVA_HOME", systemJavaHome); - }); - Platforms.onWindows(() -> { - sh.getEnv().put("JAVA_HOME", systemJavaHome); - }); - } - return sh; - } - /** * Run the command to start Elasticsearch, but don't wait or test for success. * This method is useful for testing failure conditions in startup. To await success, @@ -290,7 +289,6 @@ public void assertElasticsearchFailure(Shell.Result result, String expectedMessa // Otherwise, error should be on shell stderr assertThat(result.stderr, containsString(expectedMessage)); - } } } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/PasswordToolsTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/PasswordToolsTests.java index 9082b19f0bd49..b08b3bfe52987 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/PasswordToolsTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/PasswordToolsTests.java @@ -59,7 +59,7 @@ public void test010Install() throws Exception { public void test20GeneratePasswords() throws Exception { assertWhileRunning(() -> { - Shell.Result result = installation.executables().elasticsearchSetupPasswords.run(sh, "auto --batch", null); + Shell.Result result = installation.executables().setupPasswordsTool.run("auto --batch", null); Map userpasses = parseUsersAndPasswords(result.stdout); for (Map.Entry userpass : userpasses.entrySet()) { String response = ServerUtils.makeRequest(Request.Get("http://localhost:9200"), userpass.getKey(), userpass.getValue()); @@ -106,7 +106,7 @@ public void test30AddBootstrapPassword() throws Exception { }); } - installation.executables().elasticsearchKeystore.run(sh, "add --stdin bootstrap.password", BOOTSTRAP_PASSWORD); + installation.executables().keystoreTool.run("add --stdin bootstrap.password", BOOTSTRAP_PASSWORD); assertWhileRunning(() -> { String response = ServerUtils.makeRequest( @@ -119,7 +119,7 @@ public void test30AddBootstrapPassword() throws Exception { public void test40GeneratePasswordsBootstrapAlreadySet() throws Exception { assertWhileRunning(() -> { - Shell.Result result = installation.executables().elasticsearchSetupPasswords.run(sh, "auto --batch", null); + Shell.Result result = installation.executables().setupPasswordsTool.run("auto --batch", null); Map userpasses = parseUsersAndPasswords(result.stdout); assertThat(userpasses, hasKey("elastic")); for (Map.Entry userpass : userpasses.entrySet()) { diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/RpmPreservationTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/RpmPreservationTests.java index 0509b1d244b1a..4448e02bd0692 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/RpmPreservationTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/RpmPreservationTests.java @@ -50,9 +50,9 @@ public static void filterDistros() { public void test10Install() throws Exception { assertRemoved(distribution()); - installation = installPackage(distribution()); + installation = installPackage(sh, distribution()); assertInstalled(distribution()); - verifyPackageInstallation(installation, distribution(), newShell()); + verifyPackageInstallation(installation, distribution(), sh); } public void test20Remove() throws Exception { @@ -71,11 +71,11 @@ public void test20Remove() throws Exception { public void test30PreserveConfig() throws Exception { final Shell sh = new Shell(); - installation = installPackage(distribution()); + installation = installPackage(sh, distribution()); assertInstalled(distribution()); - verifyPackageInstallation(installation, distribution(), newShell()); + verifyPackageInstallation(installation, distribution(), sh); - sh.run("echo foobar | " + installation.executables().elasticsearchKeystore + " add --stdin foo.bar"); + sh.run("echo foobar | " + installation.executables().keystoreTool + " add --stdin foo.bar"); Stream.of( "elasticsearch.yml", "jvm.options", diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/SqlCliTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/SqlCliTests.java index 62a00aab59d22..afcc7dd0b0919 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/SqlCliTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/SqlCliTests.java @@ -38,7 +38,7 @@ public void test010Install() throws Exception { } public void test020Help() throws Exception { - Shell.Result result = installation.executables().elasticsearchSqlCli.run(sh, "--help"); + Shell.Result result = installation.executables().sqlCli.run("--help"); assertThat(result.stdout, containsString("Elasticsearch SQL CLI")); } } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/WindowsServiceTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/WindowsServiceTests.java index 506a7313cb17c..5f4fac5f5352c 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/WindowsServiceTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/WindowsServiceTests.java @@ -96,7 +96,7 @@ private void assertExit(Result result, String script, int exitCode) { } public void test10InstallArchive() throws Exception { - installation = installArchive(distribution()); + installation = installArchive(sh, distribution()); verifyArchiveInstallation(installation, distribution()); serviceScript = installation.bin("elasticsearch-service.bat").toString(); } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java index 1ec62d20a050d..28234bd118701 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java @@ -67,13 +67,11 @@ public class Archives { * errors to the console if they occur before the logging framework is initialized. */ public static final String ES_STARTUP_SLEEP_TIME_SECONDS = "10"; - public static Installation installArchive(Distribution distribution) throws Exception { - return installArchive(distribution, getDefaultArchiveInstallPath(), getCurrentVersion()); + public static Installation installArchive(Shell sh, Distribution distribution) throws Exception { + return installArchive(sh, distribution, getDefaultArchiveInstallPath(), getCurrentVersion()); } - public static Installation installArchive(Distribution distribution, Path fullInstallPath, String version) throws Exception { - final Shell sh = new Shell(); - + public static Installation installArchive(Shell sh, Distribution distribution, Path fullInstallPath, String version) throws Exception { final Path distributionFile = getDistributionFile(distribution); final Path baseInstallPath = fullInstallPath.getParent(); final Path extractedPath = baseInstallPath.resolve("elasticsearch-" + version); @@ -115,7 +113,7 @@ public static Installation installArchive(Distribution distribution, Path fullIn sh.chown(fullInstallPath); - return Installation.ofArchive(distribution, fullInstallPath); + return Installation.ofArchive(sh, distribution, fullInstallPath); } private static void setupArchiveUsersLinux(Path installPath) { diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java index 91b010957011b..245806363c6ab 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java @@ -105,7 +105,7 @@ public static Installation runContainer(Distribution distribution, Map getEnv() { return env; } @@ -112,6 +120,7 @@ private static String[] powershellCommand(String script) { } private Result runScript(String[] command) { + logger.warn("Running command with env: " + env); Result result = runScriptIgnoreExitCode(command); if (result.isSuccess() == false) { throw new RuntimeException("Command was not successful: [" + String.join(" ", command) + "]\n result: " + result.toString()); From 8513af80c2d237d71d641155990492bbd313e76f Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 12 Dec 2019 11:57:32 +1100 Subject: [PATCH 165/686] Remove reserved roles for code search (#50068) The "code_user" and "code_admin" reserved roles existed to support code search which is no longer included in Kibana. The "kibana_system" role included privileges to read/write from the code search indices, but no longer needs that access. Resolves: #49842 --- .../SecurityDocumentationIT.java | 4 +- .../authz/store/ReservedRolesStore.java | 13 ---- .../authz/store/ReservedRolesStoreTests.java | 60 +------------------ 3 files changed, 4 insertions(+), 73 deletions(-) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index 372be2c2d08a5..ec2043d3da296 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -694,8 +694,8 @@ public void testGetRoles() throws Exception { List roles = response.getRoles(); assertNotNull(response); - // 29 system roles plus the three we created - assertThat(roles.size(), equalTo(33)); + // 28 system roles plus the three we created + assertThat(roles.size(), equalTo(28 + 3)); } { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index 7e06bbf64d999..d9db50678c160 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -122,9 +122,6 @@ private static Map initializeReservedRoles() { .indices(".monitoring-*").privileges("read", "read_cross_cluster").build(), RoleDescriptor.IndicesPrivileges.builder() .indices(".management-beats").privileges("create_index", "read", "write").build(), - // .code_internal-* is for Code's internal worker queue index creation. - RoleDescriptor.IndicesPrivileges.builder() - .indices(".code-*", ".code_internal-*").privileges("all").build(), // .apm-* is for APM's agent configuration index creation RoleDescriptor.IndicesPrivileges.builder() .indices(".apm-agent-configuration").privileges("all").build(), @@ -253,16 +250,6 @@ private static Map initializeReservedRoles() { null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) .put("rollup_admin", new RoleDescriptor("rollup_admin", new String[] { "manage_rollup" }, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) - .put("code_admin", new RoleDescriptor("code_admin", new String[] {}, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder() - .indices(".code-*").privileges("all").build() - }, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) - .put("code_user", new RoleDescriptor("code_user", new String[] {}, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder() - .indices(".code-*").privileges("read").build() - }, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) .put("snapshot_user", new RoleDescriptor("snapshot_user", new String[] { "create_snapshot", GetRepositoriesAction.NAME }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("*") diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index 533962efd5a31..b2d32e871e15b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -200,8 +200,8 @@ public void testIsReserved() { assertThat(ReservedRolesStore.isReserved(RemoteMonitoringUser.COLLECTION_ROLE_NAME), is(true)); assertThat(ReservedRolesStore.isReserved(RemoteMonitoringUser.INDEXING_ROLE_NAME), is(true)); assertThat(ReservedRolesStore.isReserved("snapshot_user"), is(true)); - assertThat(ReservedRolesStore.isReserved("code_admin"), is(true)); - assertThat(ReservedRolesStore.isReserved("code_user"), is(true)); + assertThat(ReservedRolesStore.isReserved("code_admin"), is(false)); + assertThat(ReservedRolesStore.isReserved("code_user"), is(false)); } public void testSnapshotUserRole() { @@ -1383,60 +1383,4 @@ public void testLogstashAdminRole() { assertThat(logstashAdminRole.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(index), is(true)); assertThat(logstashAdminRole.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(index), is(true)); } - - public void testCodeAdminRole() { - RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("code_admin"); - assertNotNull(roleDescriptor); - assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); - - Role codeAdminRole = Role.builder(roleDescriptor, null).build(); - - assertThat(codeAdminRole.cluster().check(DelegatePkiAuthenticationAction.NAME, mock(TransportRequest.class), - mock(Authentication.class)), is(false)); - - assertThat(codeAdminRole.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false)); - assertThat(codeAdminRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(".reporting"), is(false)); - assertThat(codeAdminRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(".code-"), is(true)); - assertThat(codeAdminRole.indices().allowedIndicesMatcher("indices:foo").test(randomAlphaOfLengthBetween(8, 24)), - is(false)); - - final String index = ".code-" + randomIntBetween(0, 5); - - assertThat(codeAdminRole.indices().allowedIndicesMatcher(DeleteAction.NAME).test(index), is(true)); - assertThat(codeAdminRole.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(index), is(true)); - assertThat(codeAdminRole.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(index), is(true)); - assertThat(codeAdminRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(index), is(true)); - assertThat(codeAdminRole.indices().allowedIndicesMatcher(GetAction.NAME).test(index), is(true)); - assertThat(codeAdminRole.indices().allowedIndicesMatcher(SearchAction.NAME).test(index), is(true)); - assertThat(codeAdminRole.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(index), is(true)); - assertThat(codeAdminRole.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(index), is(true)); - } - - public void testCodeUserRole() { - RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("code_user"); - assertNotNull(roleDescriptor); - assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); - - Role codeUserRole = Role.builder(roleDescriptor, null).build(); - - assertThat(codeUserRole.cluster().check(DelegatePkiAuthenticationAction.NAME, mock(TransportRequest.class), - mock(Authentication.class)), is(false)); - - assertThat(codeUserRole.indices().allowedIndicesMatcher(SearchAction.NAME).test("foo"), is(false)); - assertThat(codeUserRole.indices().allowedIndicesMatcher(SearchAction.NAME).test(".reporting"), is(false)); - assertThat(codeUserRole.indices().allowedIndicesMatcher(SearchAction.NAME).test(".code-"), is(true)); - assertThat(codeUserRole.indices().allowedIndicesMatcher("indices:foo").test(randomAlphaOfLengthBetween(8, 24)), - is(false)); - - final String index = ".code-" + randomIntBetween(0, 5); - - assertThat(codeUserRole.indices().allowedIndicesMatcher(DeleteAction.NAME).test(index), is(false)); - assertThat(codeUserRole.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(index), is(false)); - assertThat(codeUserRole.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(index), is(false)); - assertThat(codeUserRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(index), is(false)); - assertThat(codeUserRole.indices().allowedIndicesMatcher(GetAction.NAME).test(index), is(true)); - assertThat(codeUserRole.indices().allowedIndicesMatcher(SearchAction.NAME).test(index), is(true)); - assertThat(codeUserRole.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(index), is(true)); - assertThat(codeUserRole.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(index), is(false)); - } } From 19dc69222efb334c89dde12c79a62d5b81b3eb9b Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 12 Dec 2019 14:37:59 +1100 Subject: [PATCH 166/686] Support "accept_enterprise" param in get license (#50067) In 7.6 (#49474) we added an "accept_enterprise" parameter to the GET _license endpoint. This commit adds the same parameter to 8.0, except that (a) it is deprecated (b) the only acceptable value is "true" This change also updates the license service to recognise 7.6 as a supported cluster version for the enterprise license type. --- .../elasticsearch/license/LicenseService.java | 2 +- .../license/RestGetLicenseAction.java | 16 ++++++ .../rest-api-spec/api/license.get.json | 5 ++ .../test/license/30_enterprise_license.yml | 50 +++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/test/license/30_enterprise_license.yml diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java index af34d31c14422..8168445f8c3ad 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java @@ -292,7 +292,7 @@ public ClusterState execute(ClusterState currentState) throws Exception { private static boolean licenseIsCompatible(License license, Version version) { if (License.LicenseType.ENTERPRISE.getTypeName().equalsIgnoreCase(license.type())) { - return version.onOrAfter(Version.V_8_0_0); + return version.onOrAfter(Version.V_7_6_0); } else { return true; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetLicenseAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetLicenseAction.java index 1aa3ccb0eab43..99ef7e4ec2c82 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetLicenseAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetLicenseAction.java @@ -6,7 +6,9 @@ package org.elasticsearch.license; +import org.apache.logging.log4j.LogManager; import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.protocol.xpack.license.GetLicenseRequest; @@ -27,6 +29,8 @@ public class RestGetLicenseAction extends BaseRestHandler { + private static final DeprecationLogger deprecationLogger = new DeprecationLogger(LogManager.getLogger(RestGetLicenseAction.class)); + RestGetLicenseAction(RestController controller) { controller.registerHandler(GET, "/_license", this); } @@ -47,6 +51,18 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC final Map overrideParams = new HashMap<>(2); overrideParams.put(License.REST_VIEW_MODE, "true"); overrideParams.put(License.LICENSE_VERSION_MODE, String.valueOf(License.VERSION_CURRENT)); + + // In 7.x, there was an opt-in flag to show "enterprise" licenses. In 8.0 the flag is deprecated and can only be true + // TODO Remove this from 9.0 + if (request.hasParam("accept_enterprise")) { + deprecationLogger.deprecatedAndMaybeLog("get_license_accept_enterprise", + "Including [accept_enterprise] in get license requests is deprecated." + + " The parameter will be removed in the next major version"); + if (request.paramAsBoolean("accept_enterprise", true) == false) { + throw new IllegalArgumentException("The [accept_enterprise] parameters may not be false"); + } + } + final ToXContent.Params params = new ToXContent.DelegatingMapParams(overrideParams, request); GetLicenseRequest getLicenseRequest = new GetLicenseRequest(); getLicenseRequest.local(request.paramAsBoolean("local", getLicenseRequest.local())); diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/license.get.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/license.get.json index 0e917208c1bbf..f3fc15845e9bd 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/license.get.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/license.get.json @@ -18,6 +18,11 @@ "local":{ "type":"boolean", "description":"Return local information, do not retrieve the state from master node (default: false)" + }, + "accept_enterprise":{ + "type":"boolean", + "deprecated":true, + "description":"Supported for backwards compatibility with 7.x. If this param is used it must be set to true" } } } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/license/30_enterprise_license.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/license/30_enterprise_license.yml new file mode 100644 index 0000000000000..5f4582c747b5b --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/license/30_enterprise_license.yml @@ -0,0 +1,50 @@ +--- +teardown: + - do: + license.post: + acknowledge: true + body: | + {"licenses":[{"uid":"3aa62ffe-36e1-4fad-bfdc-9dff8301eb22","type":"trial","issue_date_in_millis":1523456691721,"expiry_date_in_millis":1838816691721,"max_nodes":5,"issued_to":"customer","issuer":"elasticsearch","signature":"AAAABAAAAA2kWNcuc+DT0lrlmYZKAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAEn6fG9y2VxKBu2T3D5hffh56kzOQODCOdhr0y2d17ZSIJMZRqO7ZywPCWNS1aR33GhfIHkTER0ysML0xMH/gXavhyRvMBndJj0UBKzuwpTawSlnxYtcqN8mSBIvJC7Ki+uJ1SpAILC2ZP9fnkRlqwXqBlTwfYn7xnZgu9DKrOWru/ipTPObo7jcePl8VTK6nWFen7/hCFDQTUFZ0jQvd+nq7A1PAcHGNxGfdbMVmAXCXgGWkRfT3clo9/vadgo+isNyh1sPq9mN7gwsvBAKtA1FrpH2EXYYbfOsSpBvUmhYMgErLg1k3/CbS0pCWLKOaX1xTMayosdZOjagU3auZXY=","start_date_in_millis":-1}]} +--- +"Installing enterprise license": + - skip: + features: warnings + + ## current license version + - do: + license.post: + acknowledge: true + body: | + {"license":{"uid":"6e57906b-a8d1-4c1f-acb7-73a16edc3934","type":"enterprise","issue_date_in_millis":1523456691721,"expiry_date_in_millis":1838816691721,"max_nodes":50,"issued_to":"rest-test","issuer":"elasticsearch","signature":"AAAABAAAAA03e8BZRVXaCV4CpPGRAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAAZNhjABV6PRfa7P7sJgn70XCGoKtAVT75yU13JvKBd/UjD4TPhuZcztqZ/tcLEPxm/TSvGlogWmnw/Rw8xs8jMpBpKsJ+LOXjHhDdvXb2y7JJhCH8nlSEblMDRXysNvWpKe60Z/hb7hS4JynEUt0EBb6ji7BL42O07PNll1EGmkfsHazfs46iV91BG1VxXksI78XgWSaA0F/h7tvrNW9PTgsUaLo06InlQ8jA1dal90AoXp+MVDOHWQjVFZzUnO87/7lEb+VXt0IwchaW17ahihJqkCtGvKpWFwpuhx9xiFvkySN/g5LIVjYCvgBkiWExQ9p0Zzg3VoSlMBnVy0BWo=","start_date_in_millis":-1}} + + - match: { license_status: "valid" } + + - do: + license.get: {} + + ## a license object has 11 attributes + - length: { license: 11 } + + ## In 8.0, the enterprise license is always reports truthfully + - match: { license.type: "enterprise" } + + - do: + warnings: + - "Including [accept_enterprise] in get license requests is deprecated. The parameter will be removed in the next major version" + license.get: + accept_enterprise: "true" + + ## a license object has 11 attributes + - length: { license: 11 } + + ## Always returns real type + - match: { license.type: "enterprise" } + + ## "false" is rejected + - do: + catch: bad_request + warnings: + - "Including [accept_enterprise] in get license requests is deprecated. The parameter will be removed in the next major version" + license.get: + accept_enterprise: "false" + From 2f70c960d30858f6d94866096eae4235b8762c8c Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Wed, 11 Dec 2019 21:30:56 -0800 Subject: [PATCH 167/686] Ensure meta and document field maps are never null in GetResult (#50112) This commit ensures deseriable a GetResult from StreamInput does not leave metaFields and documentFields null. This could cause an NPE in situations where upsert response for a document that did not exist is passed back to a node that forwarded the upsert request. closes #48215 --- .../org/elasticsearch/index/get/GetResult.java | 17 +++++++---------- .../elasticsearch/index/get/GetResultTests.java | 13 +++++++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/get/GetResult.java b/server/src/main/java/org/elasticsearch/index/get/GetResult.java index f64aa03b9c487..949cc1969e7ae 100644 --- a/server/src/main/java/org/elasticsearch/index/get/GetResult.java +++ b/server/src/main/java/org/elasticsearch/index/get/GetResult.java @@ -65,8 +65,8 @@ public class GetResult implements Writeable, Iterable, ToXContent private long seqNo; private long primaryTerm; private boolean exists; - private Map documentFields; - private Map metaFields; + private final Map documentFields; + private final Map metaFields; private Map sourceAsMap; private BytesReference source; private byte[] sourceAsBytes; @@ -95,6 +95,9 @@ public GetResult(StreamInput in) throws IOException { metaFields = new HashMap<>(); splitFieldsByMetadata(fields, documentFields, metaFields); } + } else { + metaFields = Collections.emptyMap(); + documentFields = Collections.emptyMap(); } } @@ -111,14 +114,8 @@ public GetResult(String index, String id, long seqNo, long primaryTerm, long ver this.version = version; this.exists = exists; this.source = source; - this.documentFields = documentFields; - if (this.documentFields == null) { - this.documentFields = emptyMap(); - } - this.metaFields = metaFields; - if (this.metaFields == null) { - this.metaFields = emptyMap(); - } + this.documentFields = documentFields == null ? emptyMap() : documentFields; + this.metaFields = metaFields == null ? emptyMap() : metaFields; } /** diff --git a/server/src/test/java/org/elasticsearch/index/get/GetResultTests.java b/server/src/test/java/org/elasticsearch/index/get/GetResultTests.java index 92cf49ac6f487..4bca0952685ef 100644 --- a/server/src/test/java/org/elasticsearch/index/get/GetResultTests.java +++ b/server/src/test/java/org/elasticsearch/index/get/GetResultTests.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.document.DocumentField; +import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; @@ -138,6 +139,18 @@ public void testToXContentEmbeddedNotFound() throws IOException { assertEquals("{\"found\":false}", originalBytes.utf8ToString()); } + public void testSerializationNotFound() throws IOException { + // serializes and deserializes with streamable, then prints back to xcontent + GetResult getResult = new GetResult("index", "id", UNASSIGNED_SEQ_NO, 0, 1, false, null, null, null); + + BytesStreamOutput out = new BytesStreamOutput(); + getResult.writeTo(out); + getResult = new GetResult(out.bytes().streamInput()); + + BytesReference originalBytes = toXContentEmbedded(getResult, XContentType.JSON, false); + assertEquals("{\"found\":false}", originalBytes.utf8ToString()); + } + public void testGetSourceAsBytes() { XContentType xContentType = randomFrom(XContentType.values()); Tuple tuple = randomGetResult(xContentType); From b1be6375947ff20dc76a7de4e66d6a0bcc1c7554 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 12 Dec 2019 10:12:03 +0100 Subject: [PATCH 168/686] Remove Unused Single Delete in BlobStoreRepository (#50024) * Remove Unused Single Delete in BlobStoreRepository There are no more production uses of the non-bulk delete or the delete that throws on missing so this commit removes both these methods. Only the bulk delete logic remains. Where the bulk delete was derived from single deletes, the single delete code was inlined into the bulk delete method. Where single delete was used in tests it was replaced by bulk deleting. --- .../blobstore/url/URLBlobContainer.java | 3 +- .../azure/AzureBlobContainer.java | 29 +++++------- .../gcs/GoogleCloudStorageBlobContainer.java | 5 -- .../gcs/GoogleCloudStorageBlobStore.java | 14 ------ .../repositories/hdfs/HdfsBlobContainer.java | 33 ++++++++----- .../repositories/s3/S3BlobContainer.java | 15 ------ .../s3/S3BlobStoreContainerTests.java | 5 -- .../common/blobstore/BlobContainer.java | 46 +------------------ .../common/blobstore/fs/FsBlobContainer.java | 31 ++++--------- .../blobstore/BlobStoreRepository.java | 4 +- .../blobstore/ChecksumBlobStoreFormat.java | 7 --- .../snapshots/SnapshotCreationException.java | 4 -- .../MockEventuallyConsistentRepository.java | 6 ++- ...ckEventuallyConsistentRepositoryTests.java | 2 +- .../ESBlobStoreContainerTestCase.java | 31 +------------ .../mockstore/BlobContainerWrapper.java | 10 ++-- .../snapshots/mockstore/MockRepository.java | 18 ++------ 17 files changed, 63 insertions(+), 200 deletions(-) diff --git a/modules/repository-url/src/main/java/org/elasticsearch/common/blobstore/url/URLBlobContainer.java b/modules/repository-url/src/main/java/org/elasticsearch/common/blobstore/url/URLBlobContainer.java index 7a74078894c92..35174557f1e9d 100644 --- a/modules/repository-url/src/main/java/org/elasticsearch/common/blobstore/url/URLBlobContainer.java +++ b/modules/repository-url/src/main/java/org/elasticsearch/common/blobstore/url/URLBlobContainer.java @@ -35,6 +35,7 @@ import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.List; import java.util.Map; /** @@ -93,7 +94,7 @@ public Map listBlobsByPrefix(String blobNamePrefix) throws * This operation is not supported by URLBlobContainer */ @Override - public void deleteBlob(String blobName) throws IOException { + public void deleteBlobsIgnoringIfNotExists(List blobNames) { throw new UnsupportedOperationException("URL repository is read only"); } diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobContainer.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobContainer.java index 15afaada84c83..2093139e115a3 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobContainer.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobContainer.java @@ -109,22 +109,6 @@ public void writeBlobAtomic(String blobName, InputStream inputStream, long blobS writeBlob(blobName, inputStream, blobSize, failIfAlreadyExists); } - @Override - public void deleteBlob(String blobName) throws IOException { - logger.trace("deleteBlob({})", blobName); - - try { - blobStore.deleteBlob(buildKey(blobName)); - } catch (StorageException e) { - if (e.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { - throw new NoSuchFileException(e.getMessage()); - } - throw new IOException(e); - } catch (URISyntaxException e) { - throw new IOException(e); - } - } - @Override public DeleteResult delete() throws IOException { try { @@ -146,7 +130,18 @@ public void deleteBlobsIgnoringIfNotExists(List blobNames) throws IOExce // Executing deletes in parallel since Azure SDK 8 is using blocking IO while Azure does not provide a bulk delete API endpoint // TODO: Upgrade to newer non-blocking Azure SDK 11 and execute delete requests in parallel that way. for (String blobName : blobNames) { - executor.execute(ActionRunnable.run(listener, () -> deleteBlobIgnoringIfNotExists(blobName))); + executor.execute(ActionRunnable.run(listener, () -> { + logger.trace("deleteBlob({})", blobName); + try { + blobStore.deleteBlob(buildKey(blobName)); + } catch (StorageException e) { + if (e.getHttpStatusCode() != HttpURLConnection.HTTP_NOT_FOUND) { + throw new IOException(e); + } + } catch (URISyntaxException e) { + throw new IOException(e); + } + })); } } try { diff --git a/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainer.java b/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainer.java index da22750242722..7a386dd1b395e 100644 --- a/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainer.java +++ b/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainer.java @@ -72,11 +72,6 @@ public void writeBlobAtomic(String blobName, InputStream inputStream, long blobS writeBlob(blobName, inputStream, blobSize, failIfAlreadyExists); } - @Override - public void deleteBlob(String blobName) throws IOException { - blobStore.deleteBlob(buildKey(blobName)); - } - @Override public DeleteResult delete() throws IOException { return blobStore.deleteDirectory(path().buildAsString()); diff --git a/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java b/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java index f4ce144127cda..bdabce295399d 100644 --- a/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java +++ b/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java @@ -314,19 +314,6 @@ private void writeBlobMultipart(BlobInfo blobInfo, InputStream inputStream, long } } - /** - * Deletes the blob from the specific bucket - * - * @param blobName name of the blob - */ - void deleteBlob(String blobName) throws IOException { - final BlobId blobId = BlobId.of(bucketName, blobName); - final boolean deleted = SocketAccess.doPrivilegedIOException(() -> client().delete(blobId)); - if (deleted == false) { - throw new NoSuchFileException("Blob [" + blobName + "] does not exist"); - } - } - /** * Deletes the given path and all its children. * @@ -403,5 +390,4 @@ private static String buildKey(String keyPath, String s) { assert s != null; return keyPath + s; } - } diff --git a/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsBlobContainer.java b/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsBlobContainer.java index 304906464dcad..832c9db7dbfcb 100644 --- a/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsBlobContainer.java +++ b/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsBlobContainer.java @@ -43,6 +43,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; final class HdfsBlobContainer extends AbstractBlobContainer { @@ -59,17 +60,6 @@ final class HdfsBlobContainer extends AbstractBlobContainer { this.bufferSize = bufferSize; } - @Override - public void deleteBlob(String blobName) throws IOException { - try { - if (store.execute(fileContext -> fileContext.delete(new Path(path, blobName), true)) == false) { - throw new NoSuchFileException("Blob [" + blobName + "] does not exist"); - } - } catch (FileNotFoundException fnfe) { - throw new NoSuchFileException("[" + blobName + "] blob not found"); - } - } - // TODO: See if we can get precise result reporting. private static final DeleteResult DELETE_RESULT = new DeleteResult(1L, 0L); @@ -79,6 +69,27 @@ public DeleteResult delete() throws IOException { return DELETE_RESULT; } + @Override + public void deleteBlobsIgnoringIfNotExists(final List blobNames) throws IOException { + IOException ioe = null; + for (String blobName : blobNames) { + try { + store.execute(fileContext -> fileContext.delete(new Path(path, blobName), true)); + } catch (final FileNotFoundException ignored) { + // This exception is ignored + } catch (IOException e) { + if (ioe == null) { + ioe = e; + } else { + ioe.addSuppressed(e); + } + } + } + if (ioe != null) { + throw ioe; + } + } + @Override public InputStream readBlob(String blobName) throws IOException { // FSDataInputStream does buffering internally diff --git a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java index f73bc7c8732cb..6afb81da5e02c 100644 --- a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java +++ b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java @@ -107,11 +107,6 @@ public void writeBlobAtomic(String blobName, InputStream inputStream, long blobS writeBlob(blobName, inputStream, blobSize, failIfAlreadyExists); } - @Override - public void deleteBlob(String blobName) throws IOException { - deleteBlobIgnoringIfNotExists(blobName); - } - @Override public DeleteResult delete() throws IOException { final AtomicLong deletedBlobs = new AtomicLong(); @@ -215,16 +210,6 @@ private static DeleteObjectsRequest bulkDelete(String bucket, List blobs return new DeleteObjectsRequest(bucket).withKeys(blobs.toArray(Strings.EMPTY_ARRAY)).withQuiet(true); } - @Override - public void deleteBlobIgnoringIfNotExists(String blobName) throws IOException { - try (AmazonS3Reference clientReference = blobStore.clientReference()) { - // There is no way to know if an non-versioned object existed before the deletion - SocketAccess.doPrivilegedVoid(() -> clientReference.client().deleteObject(blobStore.bucket(), buildKey(blobName))); - } catch (final AmazonClientException e) { - throw new IOException("Exception when deleting blob [" + blobName + "]", e); - } - } - @Override public Map listBlobsByPrefix(@Nullable String blobNamePrefix) throws IOException { try (AmazonS3Reference clientReference = blobStore.clientReference()) { diff --git a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreContainerTests.java b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreContainerTests.java index 367ed06dc6551..95f51091555c2 100644 --- a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreContainerTests.java +++ b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreContainerTests.java @@ -71,11 +71,6 @@ protected BlobStore newBlobStore() { return randomMockS3BlobStore(); } - @Override - public void testDeleteBlob() { - assumeFalse("not implemented because of S3's weak consistency model", true); - } - @Override public void testVerifyOverwriteFails() { assumeFalse("not implemented because of S3's weak consistency model", true); 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 83de4aba8e629..8ef9badd112f6 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java @@ -89,17 +89,6 @@ public interface BlobContainer { */ void writeBlobAtomic(String blobName, InputStream inputStream, long blobSize, boolean failIfAlreadyExists) throws IOException; - /** - * Deletes the blob with the given name, if the blob exists. If the blob does not exist, - * this method may throw a {@link NoSuchFileException} if the underlying implementation supports an existence check before delete. - * - * @param blobName - * The name of the blob to delete. - * @throws NoSuchFileException if the blob does not exist - * @throws IOException if the blob exists but could not be deleted. - */ - void deleteBlob(String blobName) throws IOException; - /** * Deletes this container and all its contents from the repository. * @@ -109,44 +98,13 @@ public interface BlobContainer { DeleteResult delete() throws IOException; /** - * Deletes the blobs with given names. Unlike {@link #deleteBlob(String)} this method will not throw an exception + * Deletes the blobs with given names. This method will not throw an exception * when one or multiple of the given blobs don't exist and simply ignore this case. * * @param blobNames The names of the blob to delete. * @throws IOException if a subset of blob exists but could not be deleted. */ - default void deleteBlobsIgnoringIfNotExists(List blobNames) throws IOException { - IOException ioe = null; - for (String blobName : blobNames) { - try { - deleteBlobIgnoringIfNotExists(blobName); - } catch (IOException e) { - if (ioe == null) { - ioe = e; - } else { - ioe.addSuppressed(e); - } - } - } - if (ioe != null) { - throw ioe; - } - } - - /** - * 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 - } - } + void deleteBlobsIgnoringIfNotExists(List blobNames) throws IOException; /** * Lists all blobs in the container. diff --git a/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobContainer.java b/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobContainer.java index d3a9731b2f656..b95cf62bcc316 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobContainer.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobContainer.java @@ -44,7 +44,9 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; @@ -112,24 +114,6 @@ public Map listBlobsByPrefix(String blobNamePrefix) throws return unmodifiableMap(builder); } - @Override - public void deleteBlob(String blobName) throws IOException { - Path blobPath = path.resolve(blobName); - if (Files.isDirectory(blobPath)) { - // delete directory recursively as long as it is empty (only contains empty directories), - // which is the reason we aren't deleting any files, only the directories on the post-visit - Files.walkFileTree(blobPath, new SimpleFileVisitor<>() { - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - } else { - Files.delete(blobPath); - } - } - @Override public DeleteResult delete() throws IOException { final AtomicLong filesDeleted = new AtomicLong(0L); @@ -153,6 +137,11 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO return new DeleteResult(filesDeleted.get(), bytesDeleted.get()); } + @Override + public void deleteBlobsIgnoringIfNotExists(List blobNames) throws IOException { + IOUtils.rm(blobNames.stream().map(path::resolve).toArray(Path[]::new)); + } + @Override public InputStream readBlob(String name) throws IOException { final Path resolvedPath = path.resolve(name); @@ -166,7 +155,7 @@ public InputStream readBlob(String name) throws IOException { @Override public void writeBlob(String blobName, InputStream inputStream, long blobSize, boolean failIfAlreadyExists) throws IOException { if (failIfAlreadyExists == false) { - deleteBlobIgnoringIfNotExists(blobName); + deleteBlobsIgnoringIfNotExists(Collections.singletonList(blobName)); } final Path file = path.resolve(blobName); try (OutputStream outputStream = Files.newOutputStream(file, StandardOpenOption.CREATE_NEW)) { @@ -189,7 +178,7 @@ public void writeBlobAtomic(final String blobName, final InputStream inputStream moveBlobAtomic(tempBlob, blobName, failIfAlreadyExists); } catch (IOException ex) { try { - deleteBlobIgnoringIfNotExists(tempBlob); + deleteBlobsIgnoringIfNotExists(Collections.singletonList(tempBlob)); } catch (IOException e) { ex.addSuppressed(e); } @@ -209,7 +198,7 @@ public void moveBlobAtomic(final String sourceBlobName, final String targetBlobN if (failIfAlreadyExists) { throw new FileAlreadyExistsException("blob [" + targetBlobPath + "] already exists, cannot overwrite"); } else { - deleteBlobIgnoringIfNotExists(targetBlobName); + deleteBlobsIgnoringIfNotExists(Collections.singletonList(targetBlobName)); } } Files.move(sourceBlobPath, targetBlobPath, StandardCopyOption.ATOMIC_MOVE); diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 838bede23bf54..448f71087f91b 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -974,9 +974,7 @@ public void endVerification(String seed) { if (isReadOnly() == false) { try { final String testPrefix = testBlobPrefix(seed); - final BlobContainer container = blobStore().blobContainer(basePath().add(testPrefix)); - container.deleteBlobsIgnoringIfNotExists(List.copyOf(container.listBlobs().keySet())); - blobStore().blobContainer(basePath()).deleteBlobIgnoringIfNotExists(testPrefix); + blobStore().blobContainer(basePath().add(testPrefix)).delete(); } catch (IOException exp) { throw new RepositoryVerificationException(metadata.name(), "cannot delete test data at " + basePath(), exp); } diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/ChecksumBlobStoreFormat.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/ChecksumBlobStoreFormat.java index 4d8a25ca8d50d..030bd6946cd3b 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/ChecksumBlobStoreFormat.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/ChecksumBlobStoreFormat.java @@ -114,13 +114,6 @@ public T read(BlobContainer blobContainer, String name) throws IOException { return readBlob(blobContainer, blobName); } - /** - * Deletes obj in the blob container - */ - public void delete(BlobContainer blobContainer, String name) throws IOException { - blobContainer.deleteBlob(blobName(name)); - } - public String blobName(String name) { return String.format(Locale.ROOT, blobNameFormat, name); } diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotCreationException.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotCreationException.java index b21ec99019cf6..4a80782575bdb 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotCreationException.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotCreationException.java @@ -28,10 +28,6 @@ */ public class SnapshotCreationException extends SnapshotException { - public SnapshotCreationException(final String repositoryName, final SnapshotId snapshotId, final Throwable cause) { - super(repositoryName, snapshotId, "failed to create snapshot", cause); - } - public SnapshotCreationException(StreamInput in) throws IOException { super(in); } diff --git a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java index 880008a96b90b..c6f2c1ee16fbf 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java +++ b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepository.java @@ -220,10 +220,12 @@ private List relevantActions(String blobPath) { } @Override - public void deleteBlob(String blobName) { + public void deleteBlobsIgnoringIfNotExists(List blobNames) { ensureNotClosed(); synchronized (context.actions) { - context.actions.add(new BlobStoreAction(Operation.DELETE, path.buildAsString() + blobName)); + for (String blobName : blobNames) { + context.actions.add(new BlobStoreAction(Operation.DELETE, path.buildAsString() + blobName)); + } } } diff --git a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java index e4e6d99c6e6f0..47e2626a3b7f3 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java @@ -91,7 +91,7 @@ public void testReadAfterDeleteAfterWriteThrows() throws IOException { final int lengthWritten = randomIntBetween(1, 100); final byte[] blobData = randomByteArrayOfLength(lengthWritten); blobContainer.writeBlob(blobName, new ByteArrayInputStream(blobData), lengthWritten, true); - blobContainer.deleteBlob(blobName); + blobContainer.deleteBlobsIgnoringIfNotExists(Collections.singletonList(blobName)); assertThrowsOnInconsistentRead(blobContainer, blobName); blobStoreContext.forceConsistent(); expectThrows(NoSuchFileException.class, () -> blobContainer.readBlob(blobName)); 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 2b273c1e6a784..3aa65cf392c3d 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java @@ -33,6 +33,7 @@ import java.nio.file.FileAlreadyExistsException; import java.nio.file.NoSuchFileException; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -120,22 +121,6 @@ public void testList() throws IOException { } } - public void testDeleteBlob() throws IOException { - try (BlobStore store = newBlobStore()) { - final String blobName = "foobar"; - final BlobContainer container = store.blobContainer(new BlobPath()); - expectThrows(NoSuchFileException.class, () -> container.deleteBlob(blobName)); - - byte[] data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); - final BytesArray bytesArray = new BytesArray(data); - writeBlob(container, blobName, bytesArray, randomBoolean()); - container.deleteBlob(blobName); // should not raise - - // blob deleted, so should raise again - expectThrows(NoSuchFileException.class, () -> container.deleteBlob(blobName)); - } - } - public void testDeleteBlobs() throws IOException { try (BlobStore store = newBlobStore()) { final List blobNames = Arrays.asList("foobar", "barfoo"); @@ -153,18 +138,6 @@ public void testDeleteBlobs() throws IOException { } } - 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"); - } - } - public void testVerifyOverwriteFails() throws IOException { try (BlobStore store = newBlobStore()) { final String blobName = "foobar"; @@ -174,7 +147,7 @@ public void testVerifyOverwriteFails() throws IOException { writeBlob(container, blobName, bytesArray, true); // should not be able to overwrite existing blob expectThrows(FileAlreadyExistsException.class, () -> writeBlob(container, blobName, bytesArray, true)); - container.deleteBlob(blobName); + container.deleteBlobsIgnoringIfNotExists(Collections.singletonList(blobName)); writeBlob(container, blobName, bytesArray, true); // after deleting the previous blob, we should be able to write to it again } } diff --git a/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/BlobContainerWrapper.java b/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/BlobContainerWrapper.java index c38ddb45ab853..8aafc16b2ccbb 100644 --- a/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/BlobContainerWrapper.java +++ b/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/BlobContainerWrapper.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.Map; public class BlobContainerWrapper implements BlobContainer { @@ -55,19 +56,14 @@ public void writeBlobAtomic(final String blobName, final InputStream inputStream delegate.writeBlobAtomic(blobName, inputStream, blobSize, failIfAlreadyExists); } - @Override - public void deleteBlob(String blobName) throws IOException { - delegate.deleteBlob(blobName); - } - @Override public DeleteResult delete() throws IOException { return delegate.delete(); } @Override - public void deleteBlobIgnoringIfNotExists(final String blobName) throws IOException { - delegate.deleteBlobIgnoringIfNotExists(blobName); + public void deleteBlobsIgnoringIfNotExists(List blobNames) throws IOException { + delegate.deleteBlobsIgnoringIfNotExists(blobNames); } @Override diff --git a/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java b/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java index 6c05cc625f5cb..7b21c878599e9 100644 --- a/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java +++ b/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/MockRepository.java @@ -312,18 +312,6 @@ public InputStream readBlob(String name) throws IOException { return super.readBlob(name); } - @Override - public void deleteBlob(String blobName) throws IOException { - maybeIOExceptionOrBlock(blobName); - super.deleteBlob(blobName); - } - - @Override - public void deleteBlobIgnoringIfNotExists(String blobName) throws IOException { - maybeIOExceptionOrBlock(blobName); - super.deleteBlobIgnoringIfNotExists(blobName); - } - @Override public DeleteResult delete() throws IOException { DeleteResult deleteResult = DeleteResult.ZERO; @@ -334,10 +322,12 @@ public DeleteResult delete() throws IOException { long deleteBlobCount = blobs.size(); long deleteByteCount = 0L; for (String blob : blobs.values().stream().map(BlobMetaData::name).collect(Collectors.toList())) { - deleteBlobIgnoringIfNotExists(blob); + maybeIOExceptionOrBlock(blob); + deleteBlobsIgnoringIfNotExists(Collections.singletonList(blob)); deleteByteCount += blobs.get(blob).length(); } - blobStore().blobContainer(path().parent()).deleteBlob(path().toArray()[path().toArray().length - 1]); + blobStore().blobContainer(path().parent()).deleteBlobsIgnoringIfNotExists( + List.of(path().toArray()[path().toArray().length - 1])); return deleteResult.add(deleteBlobCount, deleteByteCount); } From e09045ef0b6f104533fe11f3b1643ac0f86132b1 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 12 Dec 2019 10:31:53 +0100 Subject: [PATCH 169/686] Better Logging GCS Blobstore Mock (#50102) * Better Logging GCS Blobstore Mock Two things: 1. We should just throw a descriptive assertion error and figure out why we're not reading a multi-part instead of returning a `400` and failing the tests that way here since we can't reproduce these 400s locally. 2. We were missing logging the exception on a cleanup delete failure that coincides with the `400` issue in tests. Relates #49429 --- .../repositories/blobstore/BlobStoreRepository.java | 2 +- .../main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 448f71087f91b..b56a22e99845b 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -1186,7 +1186,7 @@ public void clusterStateProcessed(String source, ClusterState oldState, ClusterS try { blobContainer().deleteBlobsIgnoringIfNotExists(oldIndexN); } catch (IOException e) { - logger.warn("Failed to clean up old index blobs {}", oldIndexN); + logger.warn(() -> new ParameterizedMessage("Failed to clean up old index blobs {}", oldIndexN), e); } })); } diff --git a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java index a374a745909a5..967c884104c96 100644 --- a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java +++ b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java @@ -189,7 +189,8 @@ public void handle(final HttpExchange exchange) throws IOException { exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length); exchange.getResponseBody().write(response); } else { - exchange.sendResponseHeaders(RestStatus.BAD_REQUEST.getStatus(), -1); + throw new AssertionError("Could not read multi-part request to [" + request + "] with headers [" + + new HashMap<>(exchange.getRequestHeaders()) + "]"); } } else if (Regex.simpleMatch("POST /upload/storage/v1/b/" + bucket + "/*uploadType=resumable*", request)) { From 6578d032accc4bee2fb78e514cfc816c1b827a9e Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 12 Dec 2019 10:52:57 +0100 Subject: [PATCH 170/686] Disable LongGCDisruptionTests on JDK11+12 (#50097) See discussion in #50047 (comment). There are reproducible issues with Thread#suspend in Jdk11 and Jdk12 for me locally and we have one failure for each on CI. Jdk8 and Jdk13 are stable though on CI and in my testing so I'd selectively disable this test here to keep the coverage. We aren't using suspend in production code so the JDK bug behind this does not affect us. Closes #50047 --- .../elasticsearch/test/disruption/LongGCDisruptionTests.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/framework/src/test/java/org/elasticsearch/test/disruption/LongGCDisruptionTests.java b/test/framework/src/test/java/org/elasticsearch/test/disruption/LongGCDisruptionTests.java index 7e27ffaab3e52..2021c1d7fab08 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/disruption/LongGCDisruptionTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/disruption/LongGCDisruptionTests.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.test.disruption; +import org.elasticsearch.bootstrap.JavaVersion; import org.elasticsearch.common.Nullable; import org.elasticsearch.test.ESTestCase; @@ -114,6 +115,8 @@ protected long getSuspendingTimeoutInMillis() { * but does keep retrying until all threads can be safely paused */ public void testNotBlockingUnsafeStackTraces() throws Exception { + assumeFalse("https://github.com/elastic/elasticsearch/issues/50047", + JavaVersion.current().equals(JavaVersion.parse("11")) || JavaVersion.current().equals(JavaVersion.parse("12"))); final String nodeName = "test_node"; LongGCDisruption disruption = new LongGCDisruption(random(), nodeName) { @Override From 6c0f663620d1c0fb3b46347c977e05cc6763d864 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Thu, 12 Dec 2019 10:57:31 +0100 Subject: [PATCH 171/686] Remove information about the latest PostingsFormat/DocValuesFormat. (#50118) This information is outdated and unused. --- .../java/org/elasticsearch/common/lucene/Lucene.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java index ce00c7755205f..ef2816777094a 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java @@ -24,8 +24,6 @@ import org.apache.lucene.analysis.core.KeywordAnalyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.codecs.CodecUtil; -import org.apache.lucene.codecs.DocValuesFormat; -import org.apache.lucene.codecs.PostingsFormat; import org.apache.lucene.document.LatLonDocValuesField; import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.index.BinaryDocValues; @@ -105,17 +103,8 @@ import java.util.Map; public class Lucene { - public static final String LATEST_DOC_VALUES_FORMAT = "Lucene70"; - public static final String LATEST_POSTINGS_FORMAT = "Lucene50"; public static final String LATEST_CODEC = "Lucene84"; - static { - Deprecated annotation = PostingsFormat.forName(LATEST_POSTINGS_FORMAT).getClass().getAnnotation(Deprecated.class); - assert annotation == null : "PostingsFromat " + LATEST_POSTINGS_FORMAT + " is deprecated" ; - annotation = DocValuesFormat.forName(LATEST_DOC_VALUES_FORMAT).getClass().getAnnotation(Deprecated.class); - assert annotation == null : "DocValuesFormat " + LATEST_DOC_VALUES_FORMAT + " is deprecated" ; - } - public static final String SOFT_DELETES_FIELD = "__soft_deletes"; public static final NamedAnalyzer STANDARD_ANALYZER = new NamedAnalyzer("_standard", AnalyzerScope.GLOBAL, new StandardAnalyzer()); From 5b5f1f0e16651c016c9bd34123886d0275d9d300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 12 Dec 2019 10:59:37 +0100 Subject: [PATCH 172/686] [DOCS] Moves data frame analytics job resource definitions into APIs (#50021) --- .../apis/analysisobjects.asciidoc | 216 ++++++++++ .../apis/delete-dfanalytics.asciidoc | 4 + .../apis/dfanalyticsresources.asciidoc | 298 ------------- .../apis/evaluate-dfanalytics.asciidoc | 111 ++++- .../apis/evaluateresources.asciidoc | 128 ------ .../apis/explain-dfanalytics.asciidoc | 54 +-- .../apis/get-dfanalytics-stats.asciidoc | 29 +- .../apis/get-dfanalytics.asciidoc | 53 +-- .../ml/df-analytics/apis/index.asciidoc | 2 + .../apis/put-dfanalytics.asciidoc | 117 ++---- .../apis/start-dfanalytics.asciidoc | 4 +- .../apis/stop-dfanalytics.asciidoc | 10 +- docs/reference/ml/ml-shared.asciidoc | 392 +++++++++++++++--- docs/reference/rest-api/defs.asciidoc | 8 +- 14 files changed, 764 insertions(+), 662 deletions(-) create mode 100644 docs/reference/ml/df-analytics/apis/analysisobjects.asciidoc delete mode 100644 docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc delete mode 100644 docs/reference/ml/df-analytics/apis/evaluateresources.asciidoc diff --git a/docs/reference/ml/df-analytics/apis/analysisobjects.asciidoc b/docs/reference/ml/df-analytics/apis/analysisobjects.asciidoc new file mode 100644 index 0000000000000..035601fe71409 --- /dev/null +++ b/docs/reference/ml/df-analytics/apis/analysisobjects.asciidoc @@ -0,0 +1,216 @@ +[role="xpack"] +[testenv="platinum"] +[[ml-dfa-analysis-objects]] +=== Analysis configuration objects + +{dfanalytics-cap} resources contain `analysis` objects. For example, when you +create a {dfanalytics-job}, you must define the type of analysis it performs. +This page lists all the available parameters that you can use in the `analysis` +object grouped by {dfanalytics} types. + + +[discrete] +[[oldetection-resources]] +==== {oldetection-cap} configuration objects + +An `outlier_detection` configuration object has the following properties: + +`compute_feature_influence`:: +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=compute-feature-influence] + +`feature_influence_threshold`:: +(Optional, double) +include::{docdir}/ml/ml-shared.asciidoc[tag=feature-influence-threshold] + +`method`:: +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=method] + +`n_neighbors`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=n-neighbors] + +`outlier_fraction`:: +(Optional, double) +include::{docdir}/ml/ml-shared.asciidoc[tag=outlier-fraction] + +`standardization_enabled`:: +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=standardization-enabled] + + +[discrete] +[[reganalysis-resources]] +==== {regression-cap} configuration objects + +[source,console] +-------------------------------------------------- +PUT _ml/data_frame/analytics/house_price_regression_analysis +{ + "source": { + "index": "houses_sold_last_10_yrs" <1> + }, + "dest": { + "index": "house_price_predictions" <2> + }, + "analysis": + { + "regression": { <3> + "dependent_variable": "price" <4> + } + } +} +-------------------------------------------------- +// TEST[skip:TBD] + +<1> Training data is taken from source index `houses_sold_last_10_yrs`. +<2> Analysis results will be output to destination index +`house_price_predictions`. +<3> The regression analysis configuration object. +<4> Regression analysis will use field `price` to train on. As no other +parameters have been specified it will train on 100% of eligible data, store its +prediction in destination index field `price_prediction` and use in-built +hyperparameter optimization to give minimum validation errors. + + +[float] +[[regression-resources-standard]] +===== Standard parameters + +`dependent_variable`:: +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=dependent-variable] ++ +-- +The data type of the field must be numeric. +-- + +`prediction_field_name`:: +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=prediction-field-name] + +`training_percent`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=training-percent] + +`randomize_seed`:: +(Optional, long) +include::{docdir}/ml/ml-shared.asciidoc[tag=randomize-seed] + + +[float] +[[regression-resources-advanced]] +===== Advanced parameters + +Advanced parameters are for fine-tuning {reganalysis}. They are set +automatically by <> +to give minimum validation error. It is highly recommended to use the default +values unless you fully understand the function of these parameters. If these +parameters are not supplied, their values are automatically tuned to give +minimum validation error. + +`eta`:: +(Optional, double) +include::{docdir}/ml/ml-shared.asciidoc[tag=eta] + +`feature_bag_fraction`:: +(Optional, double) +include::{docdir}/ml/ml-shared.asciidoc[tag=feature-bag-fraction] + +`maximum_number_trees`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=maximum-number-trees] + +`gamma`:: +(Optional, double) +include::{docdir}/ml/ml-shared.asciidoc[tag=gamma] + +`lambda`:: +(Optional, double) +include::{docdir}/ml/ml-shared.asciidoc[tag=lambda] + + +[discrete] +[[classanalysis-resources]] +==== {classification-cap} configuration objects + + +[float] +[[classification-resources-standard]] +===== Standard parameters + +`dependent_variable`:: +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=dependent-variable] ++ +-- +The data type of the field must be numeric or boolean. +-- + +`num_top_classes`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=num-top-classes] + +`prediction_field_name`:: +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=prediction-field-name] + +`training_percent`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=training-percent] + +`randomize_seed`:: +(Optional, long) +include::{docdir}/ml/ml-shared.asciidoc[tag=randomize-seed] + + +[float] +[[classification-resources-advanced]] +===== Advanced parameters + +Advanced parameters are for fine-tuning {classanalysis}. They are set +automatically by <> +to give minimum validation error. It is highly recommended to use the default +values unless you fully understand the function of these parameters. If these +parameters are not supplied, their values are automatically tuned to give +minimum validation error. + +`eta`:: +(Optional, double) +include::{docdir}/ml/ml-shared.asciidoc[tag=eta] + +`feature_bag_fraction`:: +(Optional, double) +include::{docdir}/ml/ml-shared.asciidoc[tag=feature-bag-fraction] + +`maximum_number_trees`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=maximum-number-trees] + +`gamma`:: +(Optional, double) +include::{docdir}/ml/ml-shared.asciidoc[tag=gamma] + +`lambda`:: +(Optional, double) +include::{docdir}/ml/ml-shared.asciidoc[tag=lambda] + +[discrete] +[[ml-hyperparam-optimization]] +==== Hyperparameter optimization + +If you don't supply {regression} or {classification} parameters, hyperparameter +optimization will be performed by default to set a value for the undefined +parameters. The starting point is calculated for data dependent parameters by +examining the loss on the training data. Subject to the size constraint, this +operation provides an upper bound on the improvement in validation loss. + +A fixed number of rounds is used for optimization which depends on the number of +parameters being optimized. The optimization starts with random search, then +Bayesian optimization is performed that is targeting maximum expected +improvement. If you override any parameters, then the optimization will +calculate the value of the remaining parameters accordingly and use the value +you provided for the overridden parameter. The number of rounds are reduced +respectively. The validation error is estimated in each round by using 4-fold +cross validation. diff --git a/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc index 3f27c91fd016f..8d80e3bba8783 100644 --- a/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc @@ -11,17 +11,20 @@ Deletes an existing {dfanalytics-job}. experimental[] + [[ml-delete-dfanalytics-request]] ==== {api-request-title} `DELETE _ml/data_frame/analytics/` + [[ml-delete-dfanalytics-prereq]] ==== {api-prereq-title} * You must have `machine_learning_admin` built-in role to use this API. For more information, see <> and <>. + [[ml-delete-dfanalytics-path-params]] ==== {api-path-parms-title} @@ -29,6 +32,7 @@ information, see <> and <>. (Required, string) include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics] + [[ml-delete-dfanalytics-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc b/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc deleted file mode 100644 index 111953b8321ab..0000000000000 --- a/docs/reference/ml/df-analytics/apis/dfanalyticsresources.asciidoc +++ /dev/null @@ -1,298 +0,0 @@ -[role="xpack"] -[testenv="platinum"] -[[ml-dfanalytics-resources]] -=== {dfanalytics-cap} job resources - -{dfanalytics-cap} resources relate to APIs such as <> and -<>. - -[discrete] -[[ml-dfanalytics-properties]] -==== {api-definitions-title} - -`analysis`:: - (object) The type of analysis that is performed on the `source`. For example: - `outlier_detection` or `regression`. For more information, see - <>. - -`analyzed_fields`:: - (Optional, object) Specify `includes` and/or `excludes` patterns to select - which fields will be included in the analysis. If `analyzed_fields` is not set, - only the relevant fields will be included. For example, all the numeric fields - for {oldetection}. For the supported field types, see <>. - Also see the <> which helps understand field selection. - - `includes`::: - (Optional, array) An array of strings that defines the fields that will be included in - the analysis. - - `excludes`::: - (Optional, array) An array of strings that defines the fields that will be excluded - from the analysis. - - -[source,console] --------------------------------------------------- -PUT _ml/data_frame/analytics/loganalytics -{ - "source": { - "index": "logdata" - }, - "dest": { - "index": "logdata_out" - }, - "analysis": { - "outlier_detection": { - } - }, - "analyzed_fields": { - "includes": [ "request.bytes", "response.counts.error" ], - "excludes": [ "source.geo" ] - } -} --------------------------------------------------- -// TEST[setup:setup_logdata] - -`description`:: - (Optional, string) A description of the job. - -`dest`:: - (object) The destination configuration of the analysis. - - `index`::: - (Required, string) Defines the _destination index_ to store the results of - the {dfanalytics-job}. - - `results_field`::: - (Optional, string) Defines the name of the field in which to store the - results of the analysis. Default to `ml`. - -`id`:: - (string) The unique identifier for the {dfanalytics-job}. This identifier can - contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and - underscores. It must start and end with alphanumeric characters. This property - is informational; you cannot change the identifier for existing jobs. - -`model_memory_limit`:: - (string) The approximate maximum amount of memory resources that are - permitted for analytical processing. The default value for {dfanalytics-jobs} - is `1gb`. If your `elasticsearch.yml` file contains an - `xpack.ml.max_model_memory_limit` setting, an error occurs when you try to - create {dfanalytics-jobs} that have `model_memory_limit` values greater than - that setting. For more information, see <>. - -`source`:: - (object) The configuration of how to source the analysis data. It requires an `index`. - Optionally, `query` and `_source` may be specified. - - `index`::: - (Required, string or array) Index or indices on which to perform the - analysis. It can be a single index or index pattern as well as an array of - indices or patterns. - - `query`::: - (Optional, object) The {es} query domain-specific language - (<>). This value corresponds to the query object in an {es} - search POST body. All the options that are supported by {es} can be used, - as this object is passed verbatim to {es}. By default, this property has - the following value: `{"match_all": {}}`. - - `_source`::: - (Optional, object) Specify `includes` and/or `excludes` patterns to select - which fields will be present in the destination. Fields that are excluded - cannot be included in the analysis. - - `includes`:::: - (array) An array of strings that defines the fields that will be included in - the destination. - - `excludes`:::: - (array) An array of strings that defines the fields that will be excluded - from the destination. - -[[dfanalytics-types]] -==== Analysis objects - -{dfanalytics-cap} resources contain `analysis` objects. For example, when you -create a {dfanalytics-job}, you must define the type of analysis it performs. - -[discrete] -[[oldetection-resources]] -==== {oldetection-cap} configuration objects - -An `outlier_detection` configuration object has the following properties: - -`compute_feature_influence`:: - (boolean) If `true`, the feature influence calculation is enabled. Defaults to - `true`. - -`feature_influence_threshold`:: - (double) The minimum {olscore} that a document needs to have in order to - calculate its {fiscore}. Value range: 0-1 (`0.1` by default). - -`method`:: - (string) Sets the method that {oldetection} uses. If the method is not set - {oldetection} uses an ensemble of different methods and normalises and - combines their individual {olscores} to obtain the overall {olscore}. We - recommend to use the ensemble method. Available methods are `lof`, `ldof`, - `distance_kth_nn`, `distance_knn`. - - `n_neighbors`:: - (integer) Defines the value for how many nearest neighbors each method of - {oldetection} will use to calculate its {olscore}. When the value is not set, - different values will be used for different ensemble members. This helps - improve diversity in the ensemble. Therefore, only override this if you are - confident that the value you choose is appropriate for the data set. - -`outlier_fraction`:: - (double) Sets the proportion of the data set that is assumed to be outlying prior to - {oldetection}. For example, 0.05 means it is assumed that 5% of values are real outliers - and 95% are inliers. - -`standardization_enabled`:: - (boolean) If `true`, then the following operation is performed on the columns - before computing outlier scores: (x_i - mean(x_i)) / sd(x_i). Defaults to - `true`. For more information, see - https://en.wikipedia.org/wiki/Feature_scaling#Standardization_(Z-score_Normalization)[this wiki page about standardization]. - - -[discrete] -[[regression-resources]] -==== {regression-cap} configuration objects - -[source,console] --------------------------------------------------- -PUT _ml/data_frame/analytics/house_price_regression_analysis -{ - "source": { - "index": "houses_sold_last_10_yrs" <1> - }, - "dest": { - "index": "house_price_predictions" <2> - }, - "analysis": - { - "regression": { <3> - "dependent_variable": "price" <4> - } - } -} --------------------------------------------------- -// TEST[skip:TBD] - -<1> Training data is taken from source index `houses_sold_last_10_yrs`. -<2> Analysis results will be output to destination index -`house_price_predictions`. -<3> The regression analysis configuration object. -<4> Regression analysis will use field `price` to train on. As no other -parameters have been specified it will train on 100% of eligible data, store its -prediction in destination index field `price_prediction` and use in-built -hyperparameter optimization to give minimum validation errors. - - -[float] -[[regression-resources-standard]] -===== Standard parameters - -include::{docdir}/ml/ml-shared.asciidoc[tag=dependent_variable] -+ --- -The data type of the field must be numeric. --- - -include::{docdir}/ml/ml-shared.asciidoc[tag=prediction_field_name] - -include::{docdir}/ml/ml-shared.asciidoc[tag=training_percent] - -include::{docdir}/ml/ml-shared.asciidoc[tag=randomize_seed] - - -[float] -[[regression-resources-advanced]] -===== Advanced parameters - -Advanced parameters are for fine-tuning {reganalysis}. They are set -automatically by <> -to give minimum validation error. It is highly recommended to use the default -values unless you fully understand the function of these parameters. If these -parameters are not supplied, their values are automatically tuned to give -minimum validation error. - -include::{docdir}/ml/ml-shared.asciidoc[tag=eta] - -include::{docdir}/ml/ml-shared.asciidoc[tag=feature_bag_fraction] - -include::{docdir}/ml/ml-shared.asciidoc[tag=maximum_number_trees] - -include::{docdir}/ml/ml-shared.asciidoc[tag=gamma] - -include::{docdir}/ml/ml-shared.asciidoc[tag=lambda] - - -[discrete] -[[classification-resources]] -==== {classification-cap} configuration objects - - -[float] -[[classification-resources-standard]] -===== Standard parameters - -include::{docdir}/ml/ml-shared.asciidoc[tag=dependent_variable] -+ --- -The data type of the field must be numeric or boolean. --- - -`num_top_classes`:: - (Optional, integer) Defines the number of categories for which the predicted - probabilities are reported. It must be non-negative. If it is greater than the - total number of categories (in the {version} version of the {stack}, it's two) - to predict then we will report all category probabilities. Defaults to 2. - -include::{docdir}/ml/ml-shared.asciidoc[tag=prediction_field_name] - -include::{docdir}/ml/ml-shared.asciidoc[tag=training_percent] - -include::{docdir}/ml/ml-shared.asciidoc[tag=randomize_seed] - - -[float] -[[classification-resources-advanced]] -===== Advanced parameters - -Advanced parameters are for fine-tuning {classanalysis}. They are set -automatically by <> -to give minimum validation error. It is highly recommended to use the default -values unless you fully understand the function of these parameters. If these -parameters are not supplied, their values are automatically tuned to give -minimum validation error. - -include::{docdir}/ml/ml-shared.asciidoc[tag=eta] - -include::{docdir}/ml/ml-shared.asciidoc[tag=feature_bag_fraction] - -include::{docdir}/ml/ml-shared.asciidoc[tag=maximum_number_trees] - -include::{docdir}/ml/ml-shared.asciidoc[tag=gamma] - -include::{docdir}/ml/ml-shared.asciidoc[tag=lambda] - - -[[ml-hyperparameter-optimization]] -===== Hyperparameter optimization - -If you don't supply {regression} or {classification} parameters, hyperparameter -optimization will be performed by default to set a value for the undefined -parameters. The starting point is calculated for data dependent parameters by -examining the loss on the training data. Subject to the size constraint, this -operation provides an upper bound on the improvement in validation loss. - -A fixed number of rounds is used for optimization which depends on the number of -parameters being optimized. The optimization starts with random search, then -Bayesian optimization is performed that is targeting maximum expected -improvement. If you override any parameters, then the optimization will -calculate the value of the remaining parameters accordingly and use the value -you provided for the overridden parameter. The number of rounds are reduced -respectively. The validation error is estimated in each round by using 4-fold -cross validation. diff --git a/docs/reference/ml/df-analytics/apis/evaluate-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/evaluate-dfanalytics.asciidoc index 4576f465a7604..bcba82992d154 100644 --- a/docs/reference/ml/df-analytics/apis/evaluate-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/evaluate-dfanalytics.asciidoc @@ -12,6 +12,7 @@ Evaluates the {dfanalytics} for an annotated index. experimental[] + [[ml-evaluate-dfanalytics-request]] ==== {api-request-title} @@ -37,26 +38,113 @@ result field to be present. [[ml-evaluate-dfanalytics-request-body]] ==== {api-request-body-title} -`index`:: - (Required, object) Defines the `index` in which the evaluation will be - performed. - -`query`:: - (Optional, object) A query clause that retrieves a subset of data from the - source index. See <>. - `evaluation`:: - (Required, object) Defines the type of evaluation you want to perform. See - <>. +(Required, object) Defines the type of evaluation you want to perform. The +value of this object can be different depending on the type of evaluation you +want to perform. See <>. + -- Available evaluation types: - * `binary_soft_classification` * `regression` * `classification` -- +`index`:: +(Required, object) Defines the `index` in which the evaluation will be +performed. + +`query`:: +(Optional, object) A query clause that retrieves a subset of data from the +source index. See <>. + +[[ml-evaluate-dfanalytics-resources]] +==== {dfanalytics-cap} evaluation resources + +[[binary-sc-resources]] +===== Binary soft classification configuration objects + +Binary soft classification evaluates the results of an analysis which outputs +the probability that each document belongs to a certain class. For example, in +the context of {oldetection}, the analysis outputs the probability whether each +document is an outlier. + +`actual_field`:: + (Required, string) The field of the `index` which contains the `ground truth`. + The data type of this field can be boolean or integer. If the data type is + integer, the value has to be either `0` (false) or `1` (true). + +`predicted_probability_field`:: + (Required, string) The field of the `index` that defines the probability of + whether the item belongs to the class in question or not. It's the field that + contains the results of the analysis. + +`metrics`:: + (Optional, object) Specifies the metrics that are used for the evaluation. + Available metrics: + + `auc_roc`:: + (Optional, object) The AUC ROC (area under the curve of the receiver + operating characteristic) score and optionally the curve. Default value is + {"includes_curve": false}. + + `precision`:: + (Optional, object) Set the different thresholds of the {olscore} at where + the metric is calculated. Default value is {"at": [0.25, 0.50, 0.75]}. + + `recall`:: + (Optional, object) Set the different thresholds of the {olscore} at where + the metric is calculated. Default value is {"at": [0.25, 0.50, 0.75]}. + + `confusion_matrix`:: + (Optional, object) Set the different thresholds of the {olscore} at where + the metrics (`tp` - true positive, `fp` - false positive, `tn` - true + negative, `fn` - false negative) are calculated. Default value is + {"at": [0.25, 0.50, 0.75]}. + + +[[regression-evaluation-resources]] +===== {regression-cap} evaluation objects + +{regression-cap} evaluation evaluates the results of a {regression} analysis +which outputs a prediction of values. + +`actual_field`:: + (Required, string) The field of the `index` which contains the `ground truth`. + The data type of this field must be numerical. + +`predicted_field`:: + (Required, string) The field in the `index` that contains the predicted value, + in other words the results of the {regression} analysis. + +`metrics`:: + (Required, object) Specifies the metrics that are used for the evaluation. + Available metrics are `r_squared` and `mean_squared_error`. + + +[[classification-evaluation-resources]] +==== {classification-cap} evaluation objects + +{classification-cap} evaluation evaluates the results of a {classanalysis} which +outputs a prediction that identifies to which of the classes each document +belongs. + +`actual_field`:: + (Required, string) The field of the `index` which contains the ground truth. + The data type of this field must be keyword. + +`metrics`:: + (Required, object) Specifies the metrics that are used for the evaluation. + Available metric is `multiclass_confusion_matrix`. + +`predicted_field`:: + (Required, string) The field in the `index` that contains the predicted value, + in other words the results of the {classanalysis}. The data type of this field + is string. You need to add `.keyword` to the predicted field name (the name + you put in the {classanalysis} object as `prediction_field_name` or the + default value of the same field if you didn't specified explicitly). For + example, `predicted_field` : `ml.animal_class_prediction.keyword`. + //// [[ml-evaluate-dfanalytics-results]] @@ -75,6 +163,7 @@ Available evaluation types: `recall`::: TBD //// + [[ml-evaluate-dfanalytics-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/df-analytics/apis/evaluateresources.asciidoc b/docs/reference/ml/df-analytics/apis/evaluateresources.asciidoc deleted file mode 100644 index a9f7d654c2756..0000000000000 --- a/docs/reference/ml/df-analytics/apis/evaluateresources.asciidoc +++ /dev/null @@ -1,128 +0,0 @@ -[role="xpack"] -[testenv="platinum"] -[[ml-evaluate-dfanalytics-resources]] -=== {dfanalytics-cap} evaluation resources - -Evaluation configuration objects relate to the <>. - -[discrete] -[[ml-evaluate-dfanalytics-properties]] -==== {api-definitions-title} - -`evaluation`:: - (object) Defines the type of evaluation you want to perform. The value of this - object can be different depending on the type of evaluation you want to - perform. -+ --- -Available evaluation types: -* `binary_soft_classification` -* `regression` -* `classification` --- - -`query`:: - (object) A query clause that retrieves a subset of data from the source index. - See <>. The evaluation only applies to those documents of the index - that match the query. - - -[[binary-sc-resources]] -==== Binary soft classification configuration objects - -Binary soft classification evaluates the results of an analysis which outputs -the probability that each document belongs to a certain class. For -example, in the context of outlier detection, the analysis outputs the -probability whether each document is an outlier. - -[discrete] -[[binary-sc-resources-properties]] -===== {api-definitions-title} - -`actual_field`:: - (string) The field of the `index` which contains the `ground truth`. - The data type of this field can be boolean or integer. If the data type is - integer, the value has to be either `0` (false) or `1` (true). - -`predicted_probability_field`:: - (string) The field of the `index` that defines the probability of - whether the item belongs to the class in question or not. It's the field that - contains the results of the analysis. - -`metrics`:: - (object) Specifies the metrics that are used for the evaluation. - Available metrics: - - `auc_roc`:: - (object) The AUC ROC (area under the curve of the receiver operating - characteristic) score and optionally the curve. - Default value is {"includes_curve": false}. - - `precision`:: - (object) Set the different thresholds of the {olscore} at where the metric - is calculated. - Default value is {"at": [0.25, 0.50, 0.75]}. - - `recall`:: - (object) Set the different thresholds of the {olscore} at where the metric - is calculated. - Default value is {"at": [0.25, 0.50, 0.75]}. - - `confusion_matrix`:: - (object) Set the different thresholds of the {olscore} at where the metrics - (`tp` - true positive, `fp` - false positive, `tn` - true negative, `fn` - - false negative) are calculated. - Default value is {"at": [0.25, 0.50, 0.75]}. - - -[[regression-evaluation-resources]] -==== {regression-cap} evaluation objects - -{regression-cap} evaluation evaluates the results of a {regression} analysis -which outputs a prediction of values. - - -[discrete] -[[regression-evaluation-resources-properties]] -===== {api-definitions-title} - -`actual_field`:: - (string) The field of the `index` which contains the `ground truth`. The data - type of this field must be numerical. - -`predicted_field`:: - (string) The field in the `index` that contains the predicted value, - in other words the results of the {regression} analysis. - -`metrics`:: - (object) Specifies the metrics that are used for the evaluation. Available - metrics are `r_squared` and `mean_squared_error`. - - -[[classification-evaluation-resources]] -==== {classification-cap} evaluation objects - -{classification-cap} evaluation evaluates the results of a {classanalysis} which -outputs a prediction that identifies to which of the classes each document -belongs. - - -[discrete] -[[classification-evaluation-resources-properties]] -===== {api-definitions-title} - -`actual_field`:: - (string) The field of the `index` which contains the ground truth. The data - type of this field must be keyword. - -`metrics`:: - (object) Specifies the metrics that are used for the evaluation. Available - metric is `multiclass_confusion_matrix`. - -`predicted_field`:: - (string) The field in the `index` that contains the predicted value, in other - words the results of the {classanalysis}. The data type of this field is - string. You need to add `.keyword` to the predicted field name (the name you - put in the {classanalysis} object as `prediction_field_name` or the default - value of the same field if you didn't specified explicitly). For example, - `predicted_field` : `ml.animal_class_prediction.keyword`. \ No newline at end of file diff --git a/docs/reference/ml/df-analytics/apis/explain-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/explain-dfanalytics.asciidoc index fb867b53ce165..980e2aa7d3fbb 100644 --- a/docs/reference/ml/df-analytics/apis/explain-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/explain-dfanalytics.asciidoc @@ -12,6 +12,7 @@ Explains a {dataframe-analytics-config}. experimental[] + [[ml-explain-dfanalytics-request]] ==== {api-request-title} @@ -23,22 +24,28 @@ experimental[] `POST _ml/data_frame/analytics//_explain` + [[ml-explain-dfanalytics-prereq]] ==== {api-prereq-title} * You must have `monitor_ml` privilege to use this API. For more information, see <> and <>. + [[ml-explain-dfanalytics-desc]] ==== {api-description-title} -This API provides explanations for a {dataframe-analytics-config} that either exists already or one that has not been created yet. +This API provides explanations for a {dataframe-analytics-config} that either +exists already or one that has not been created yet. The following explanations are provided: -* which fields are included or not in the analysis and why -* how much memory is estimated to be required. The estimate can be used when deciding the appropriate value for `model_memory_limit` setting later on. +* which fields are included or not in the analysis and why, +* how much memory is estimated to be required. The estimate can be used when + deciding the appropriate value for `model_memory_limit` setting later on, + about either an existing {dfanalytics-job} or one that has not been created yet. + [[ml-explain-dfanalytics-path-params]] ==== {api-path-parms-title} @@ -46,13 +53,14 @@ about either an existing {dfanalytics-job} or one that has not been created yet. (Optional, string) include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics] + [[ml-explain-dfanalytics-request-body]] ==== {api-request-body-title} `data_frame_analytics_config`:: - (Optional, object) Intended configuration of {dfanalytics-job}. For more information, see - <>. - Note that `id` and `dest` don't need to be provided in the context of this API. + (Optional, object) Intended configuration of {dfanalytics-job}. Note that `id` + and `dest` don't need to be provided in the context of this API. + [[ml-explain-dfanalytics-results]] ==== {api-response-body-title} @@ -60,38 +68,13 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics] The API returns a response that contains the following: `field_selection`:: - (array) An array of objects that explain selection for each field, sorted by the field names. - Each object in the array has the following properties: - - `name`::: - (string) The field name. - - `mapping_types`::: - (string) The mapping types of the field. - - `is_included`::: - (boolean) Whether the field is selected to be included in the analysis. - - `is_required`::: - (boolean) Whether the field is required. - - `feature_type`::: - (string) The feature type of this field for the analysis. May be `categorical` or `numerical`. - - `reason`::: - (string) The reason a field is not selected to be included in the analysis. +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=field-selection] `memory_estimation`:: - (object) An object containing the memory estimates. The object has the following properties: - - `expected_memory_without_disk`::: - (string) Estimated memory usage under the assumption that the whole {dfanalytics} should happen in memory - (i.e. without overflowing to disk). +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=memory-estimation] - `expected_memory_with_disk`::: - (string) Estimated memory usage under the assumption that overflowing to disk is allowed during {dfanalytics}. - `expected_memory_with_disk` is usually smaller than `expected_memory_without_disk` as using disk allows to - limit the main memory needed to perform {dfanalytics}. [[ml-explain-dfanalytics-example]] ==== {api-examples-title} @@ -114,6 +97,7 @@ POST _ml/data_frame/analytics/_explain -------------------------------------------------- // TEST[skip:TBD] + The API returns the following results: [source,console-result] diff --git a/docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc b/docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc index dcdb66689936f..91c022e9b64b1 100644 --- a/docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc +++ b/docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc @@ -48,12 +48,12 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics-default] include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-match] `from`:: - (Optional, integer) Skips the specified number of {dfanalytics-jobs}. The - default value is `0`. +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=from] `size`:: - (Optional, integer) Specifies the maximum number of {dfanalytics-jobs} to - obtain. The default value is `100`. +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=size] [[ml-get-dfanalytics-stats-response-body]] @@ -62,25 +62,8 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-match] The API returns the following information: `data_frame_analytics`:: - (array) An array of statistics objects for {dfanalytics-jobs}, which are - sorted by the `id` value in ascending order. - - `id`:: - (string) The unique identifier of the {dfanalytics-job}. - - `state`:: - (string) Current state of the {dfanalytics-job}. - - `progress`:: - (array) The progress report of the {dfanalytics-job} by phase. - - `phase`:: - (string) Defines the phase of the {dfanalytics-job}. Possible phases: - `reindexing`, `loading_data`, `analyzing`, and `writing_results`. - - `progress_percent`:: - (integer) The progress that the {dfanalytics-job} has made expressed in - percentage. +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=data-frame-analytics-stats] [[ml-get-dfanalytics-stats-response-codes]] diff --git a/docs/reference/ml/df-analytics/apis/get-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/get-dfanalytics.asciidoc index fda6039f88cf3..816135fb62263 100644 --- a/docs/reference/ml/df-analytics/apis/get-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/get-dfanalytics.asciidoc @@ -11,6 +11,7 @@ Retrieves configuration information for {dfanalytics-jobs}. experimental[] + [[ml-get-dfanalytics-request]] ==== {api-request-title} @@ -22,11 +23,13 @@ experimental[] `GET _ml/data_frame/analytics/_all` + [[ml-get-dfanalytics-prereq]] ==== {api-prereq-title} -* You must have `monitor_ml` privilege to use this API. For more -information, see <> and <>. +* You must have `monitor_ml` privilege to use this API. For more information, +see <> and <>. + [[ml-get-dfanalytics-desc]] ==== {api-description-title} @@ -34,47 +37,44 @@ information, see <> and <>. You can get information for multiple {dfanalytics-jobs} in a single API request by using a comma-separated list of {dfanalytics-jobs} or a wildcard expression. + [[ml-get-dfanalytics-path-params]] ==== {api-path-parms-title} ``:: - (Optional, string) Identifier for the {dfanalytics-job}. If you do not specify - one of these options, the API returns information for the first hundred - {dfanalytics-jobs}. You can get information for all {dfanalytics-jobs} by - using _all, by specifying `*` as the ``, or by - omitting the ``. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics-default] ++ +-- +You can get information for all {dfanalytics-jobs} by using _all, by specifying +`*` as the ``, or by omitting the +``. +-- + [[ml-get-dfanalytics-query-params]] ==== {api-query-parms-title} `allow_no_match`:: - (Optional, boolean) Specifies what to do when the request: -+ --- -* Contains wildcard expressions and there are no {dfanalytics-jobs} that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns an empty `data_frame_analytics` array -when there are no matches and the subset of results when there are partial -matches. If this parameter is `false`, the request returns a `404` status code -when there are no matches or only partial matches. --- +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-match] `from`:: - (Optional, integer) Skips the specified number of {dfanalytics-jobs}. The - default value is `0`. +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=from] `size`:: - (Optional, integer) Specifies the maximum number of {dfanalytics-jobs} to - obtain. The default value is `100`. - +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=size] + + [[ml-get-dfanalytics-results]] ==== {api-response-body-title} `data_frame_analytics`:: - (array) An array of {dfanalytics-job} resources. For more information, see - <>. +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=data-frame-analytics] + [[ml-get-dfanalytics-response-codes]] ==== {api-response-codes-title} @@ -83,6 +83,7 @@ when there are no matches or only partial matches. If `allow_no_match` is `false`, this code indicates that there are no resources that match the request or only partial matches for the request. + [[ml-get-dfanalytics-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/df-analytics/apis/index.asciidoc b/docs/reference/ml/df-analytics/apis/index.asciidoc index 6bf63e7ddb8c0..bebd6d3aae820 100644 --- a/docs/reference/ml/df-analytics/apis/index.asciidoc +++ b/docs/reference/ml/df-analytics/apis/index.asciidoc @@ -14,6 +14,8 @@ You can use the following APIs to perform {ml} {dfanalytics} activities. * <> * <> +For the `analysis` object resources, check <>. + See also <>. //CREATE diff --git a/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc index 123eb6633e37b..d549e6824002e 100644 --- a/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc @@ -93,91 +93,55 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics-define] ==== {api-request-body-title} `analysis`:: - (Required, object) Defines the type of {dfanalytics} you want to perform on - your source index. For example: `outlier_detection`. See - <>. +(Required, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=analysis] `analyzed_fields`:: - (Optional, object) Specify `includes` and/or `excludes` patterns to select - which fields will be included in the analysis. If `analyzed_fields` is not - set, only the relevant fields will be included. For example, all the numeric - fields for {oldetection}. For the supported field types, see - <>. Also see the <> - which helps understand field selection. - - `includes`::: - (Optional, array) An array of strings that defines the fields that will be - included in the analysis. - - `excludes`::: - (Optional, array) An array of strings that defines the fields that will be - excluded from the analysis. You do not need to add fields with unsupported - data types to `excludes`, these fields are excluded from the analysis - automatically. +(Optional, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=analyzed-fields] + +[source,console] +-------------------------------------------------- +PUT _ml/data_frame/analytics/loganalytics +{ + "source": { + "index": "logdata" + }, + "dest": { + "index": "logdata_out" + }, + "analysis": { + "outlier_detection": { + } + }, + "analyzed_fields": { + "includes": [ "request.bytes", "response.counts.error" ], + "excludes": [ "source.geo" ] + } +} +-------------------------------------------------- +// TEST[setup:setup_logdata] + `description`:: - (Optional, string) A description of the job. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=description-dfa] `dest`:: - (Required, object) The destination configuration, consisting of `index` and - optionally `results_field` (`ml` by default). - - `index`::: - (Required, string) Defines the _destination index_ to store the results of - the {dfanalytics-job}. - - `results_field`::: - (Optional, string) Defines the name of the field in which to store the - results of the analysis. Default to `ml`. +(Required, object) +include::{docdir}/ml/ml-shared.asciidoc[tag=dest] `model_memory_limit`:: - (Optional, string) The approximate maximum amount of memory resources that are - permitted for analytical processing. The default value for {dfanalytics-jobs} - is `1gb`. If your `elasticsearch.yml` file contains an - `xpack.ml.max_model_memory_limit` setting, an error occurs when you try to - create {dfanalytics-jobs} that have `model_memory_limit` values greater than - that setting. For more information, see <>. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-memory-limit-dfa] `source`:: - (object) The configuration of how to source the analysis data. It requires an - `index`. Optionally, `query` and `_source` may be specified. - - `index`::: - (Required, string or array) Index or indices on which to perform the - analysis. It can be a single index or index pattern as well as an array of - indices or patterns. - - `query`::: - (Optional, object) The {es} query domain-specific language - (<>). This value corresponds to the query object in an {es} - search POST body. All the options that are supported by {es} can be used, - as this object is passed verbatim to {es}. By default, this property has - the following value: `{"match_all": {}}`. - - `_source`::: - (Optional, object) Specify `includes` and/or `excludes` patterns to select - which fields will be present in the destination. Fields that are excluded - cannot be included in the analysis. - - `includes`:::: - (array) An array of strings that defines the fields that will be - included in the destination. - - `excludes`:::: - (array) An array of strings that defines the fields that will be - excluded from the destination. +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=source-put-dfa] `allow_lazy_start`:: - (Optional, boolean) Whether this job should be allowed to start when there - is insufficient {ml} node capacity for it to be immediately assigned to a node. - The default is `false`, which means that the <> - will return an error if a {ml} node with capacity to run the - job cannot immediately be found. (However, this is also subject to - the cluster-wide `xpack.ml.max_lazy_ml_nodes` setting - see - <>.) If this option is set to `true` then - the <> will not return an error, and the job will - wait in the `starting` state until sufficient {ml} node capacity - is available. +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-lazy-start] [[ml-put-dfanalytics-example]] @@ -405,9 +369,10 @@ PUT _ml/data_frame/analytics/student_performance_mathematics_0.3 -------------------------------------------------- // TEST[skip:TBD] -<1> The `training_percent` defines the percentage of the data set that will be used -for training the model. -<2> The `randomize_seed` is the seed used to randomly pick which data is used for training. +<1> The `training_percent` defines the percentage of the data set that will be +used for training the model. +<2> The `randomize_seed` is the seed used to randomly pick which data is used +for training. [[ml-put-dfanalytics-example-c]] diff --git a/docs/reference/ml/df-analytics/apis/start-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/start-dfanalytics.asciidoc index ba8b4169034a4..6e1fd7ffe1a44 100644 --- a/docs/reference/ml/df-analytics/apis/start-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/start-dfanalytics.asciidoc @@ -36,8 +36,8 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics-define] ==== {api-query-parms-title} `timeout`:: - (Optional, time) Controls the amount of time to wait until the - {dfanalytics-job} starts. The default value is 20 seconds. +(Optional, <>) +include::{docdir}/ml/ml-shared.asciidoc[tag=timeout-start] [[ml-start-dfanalytics-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/df-analytics/apis/stop-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/stop-dfanalytics.asciidoc index ce3a932f1b07d..7ea707b9d1b7f 100644 --- a/docs/reference/ml/df-analytics/apis/stop-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/stop-dfanalytics.asciidoc @@ -49,16 +49,16 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics-define] ==== {api-query-parms-title} `allow_no_match`:: - (Optional, boolean) If `false` and the `data_frame_analytics_id` does not - match any {dfanalytics-job} an error will be returned. The default value is - `true`. +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-match] + `force`:: (Optional, boolean) If true, the {dfanalytics-job} is stopped forcefully. `timeout`:: - (Optional, time) Controls the amount of time to wait until the - {dfanalytics-job} stops. The default value is 20 seconds. +(Optional, <>) +include::{docdir}/ml/ml-shared.asciidoc[tag=timeout-stop] [[ml-stop-dfanalytics-example]] diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index 65fd370dda9ad..c13b4e903b91e 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -8,7 +8,8 @@ end::aggregations[] tag::allow-lazy-open[] Advanced configuration option. Specifies whether this job can open when there is insufficient {ml} node capacity for it to be immediately assigned to a node. The -default value is `false`; if a {ml} node with capacity to run the job cannot immediately be found, the <> returns an +default value is `false`; if a {ml} node with capacity to run the job cannot +immediately be found, the <> returns an error. However, this is also subject to the cluster-wide `xpack.ml.max_lazy_ml_nodes` setting; see <>. If this option is set to `true`, the <> does not @@ -16,6 +17,18 @@ return an error and the job waits in the `opening` state until sufficient {ml} node capacity is available. end::allow-lazy-open[] + +tag::allow-lazy-start[] +Whether this job should be allowed to start when there is insufficient {ml} node +capacity for it to be immediately assigned to a node. The default is `false`, +which means that the <> will return an error if a {ml} node +with capacity to run the job cannot immediately be found. (However, this is also +subject to the cluster-wide `xpack.ml.max_lazy_ml_nodes` setting - see +<>.) If this option is set to `true` then the +<> will not return an error, and the job will wait in the +`starting` state until sufficient {ml} node capacity is available. +end::allow-lazy-start[] + tag::allow-no-datafeeds[] Specifies what to do when the request: + @@ -61,10 +74,16 @@ when there are no matches or only partial matches. -- end::allow-no-match[] +tag::analysis[] +Defines the type of {dfanalytics} you want to perform on your source index. For +example: `outlier_detection`. See <>. +end::analysis[] + tag::analysis-config[] The analysis configuration, which specifies how to analyze the data. After you create a job, you cannot change the analysis configuration; all -the properties are informational. An analysis configuration object has the following properties: +the properties are informational. An analysis configuration object has the +following properties: `bucket_span`::: (<>) @@ -128,6 +147,25 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-examples-limit] include::{docdir}/ml/ml-shared.asciidoc[tag=model-memory-limit] end::analysis-limits[] +tag::analyzed-fields[] +Specify `includes` and/or `excludes` patterns to select which fields will be +included in the analysis. If `analyzed_fields` is not set, only the relevant +fields will be included. For example, all the numeric fields for {oldetection}. +For the supported field types, see <>. Also +see the <> which helps understand field selection. + +`includes`::: + (Optional, array) An array of strings that defines the fields that will be + included in the analysis. + +`excludes`::: + (Optional, array) An array of strings that defines the fields that will be + excluded from the analysis. You do not need to add fields with unsupported + data types to `excludes`, these fields are excluded from the analysis + automatically. +end::analyzed-fields[] + + tag::background-persist-interval[] Advanced configuration option. The time between each periodic persistence of the model. The default value is a randomized value between 3 to 4 hours, which @@ -208,7 +246,9 @@ tag::categorization-filters[] If `categorization_field_name` is specified, you can also define optional filters. This property expects an array of regular expressions. The expressions are used to filter out matching sequences from the categorization field values. -You can use this functionality to fine tune the categorization by excluding sequences from consideration when categories are defined. For example, you can exclude SQL statements that appear in your log files. For more information, see +You can use this functionality to fine tune the categorization by excluding +sequences from consideration when categories are defined. For example, you can +exclude SQL statements that appear in your log files. For more information, see {stack-ov}/ml-configuring-categories.html[Categorizing log messages]. This property cannot be used at the same time as `categorization_analyzer`. If you only want to define simple regular expression filters that are applied prior to @@ -229,6 +269,11 @@ add them here as <>. end::char-filter[] + +tag::compute-feature-influence[] +If `true`, the feature influence calculation is enabled. Defaults to `true`. +end::compute-feature-influence[] + tag::chunking-config[] {dfeeds-cap} might be required to search over long time periods, for several months or years. This search is split into time chunks in order to ensure the load @@ -280,7 +325,8 @@ to an object with the following properties: `filter_type`::: (string) Either `include` (the rule applies for values in the filter) or -`exclude` (the rule applies for values not in the filter). Defaults to `include`. +`exclude` (the rule applies for values not in the filter). Defaults to +`include`. `conditions`:: (array) An optional array of numeric conditions when the rule applies. A rule @@ -339,6 +385,92 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=time-format] -- end::data-description[] +tag::data-frame-analytics[] +An array of {dfanalytics-job} resources, which are sorted by the `id` value in +ascending order. + +`id`::: +(string) The unique identifier of the {dfanalytics-job}. + +`source`::: +(object) The configuration of how the analysis data is sourced. It has an +`index` parameter and optionally a `query` and a `_source`. + +`index`:::: +(array) Index or indices on which to perform the analysis. It can be a single +index or index pattern as well as an array of indices or patterns. + +`query`:::: +(object) The query that has been specified for the {dfanalytics-job}. The {es} +query domain-specific language (<>). This value corresponds to +the query object in an {es} search POST body. By default, this property has the +following value: `{"match_all": {}}`. + +`_source`:::: +(object) Contains the specified `includes` and/or `excludes` patterns that +select which fields are present in the destination. Fields that are excluded +cannot be included in the analysis. + +`includes`::::: +(array) An array of strings that defines the fields that are included in the +destination. + +`excludes`::::: +(array) An array of strings that defines the fields that are excluded from the +destination. + +`dest`::: +(string) The destination configuration of the analysis. + +`index`:::: +(string) The _destination index_ that stores the results of the +{dfanalytics-job}. + +`results_field`:::: +(string) The name of the field that stores the results of the analysis. Defaults +to `ml`. + +`analysis`::: +(object) The type of analysis that is performed on the `source`. + +`analyzed_fields`::: +(object) Contains `includes` and/or `excludes` patterns that select which fields +are included in the analysis. + +`includes`:::: +(Optional, array) An array of strings that defines the fields that are included +in the analysis. + +`excludes`:::: +(Optional, array) An array of strings that defines the fields that are excluded +from the analysis. + +`model_memory_limit`::: +(string) The `model_memory_limit` that has been set to the {dfanalytics-job}. +end::data-frame-analytics[] + +tag::data-frame-analytics-stats[] +An array of statistics objects for {dfanalytics-jobs}, which are +sorted by the `id` value in ascending order. + +`id`::: +(string) The unique identifier of the {dfanalytics-job}. + +`state`::: +(string) Current state of the {dfanalytics-job}. + +`progress`::: +(array) The progress report of the {dfanalytics-job} by phase. + +`phase`::: +(string) Defines the phase of the {dfanalytics-job}. Possible phases: +`reindexing`, `loading_data`, `analyzing`, and `writing_results`. + +`progress_percent`::: +(integer) The progress that the {dfanalytics-job} has made expressed in +percentage. +end::data-frame-analytics-stats[] + tag::datafeed-id[] A numerical character string that uniquely identifies the {dfeed}. This identifier can contain lowercase alphanumeric characters (a-z @@ -380,14 +512,30 @@ calculation is based on the maximum of `2h` or `8 * bucket_span`. -- end::delayed-data-check-config[] -tag::dependent_variable[] -`dependent_variable`:: -(Required, string) Defines which field of the document is to be predicted. +tag::dependent-variable[] +Defines which field of the document is to be predicted. This parameter is supplied by field name and must match one of the fields in the index being used to train. If this field is missing from a document, then that document will not be used for training, but a prediction with the trained model will be generated for it. It is also known as continuous target variable. -end::dependent_variable[] +end::dependent-variable[] + +tag::description-dfa[] +A description of the job. +end::description-dfa[] + +tag::dest[] +The destination configuration, consisting of `index` and +optionally `results_field` (`ml` by default). + + `index`::: + (Required, string) Defines the _destination index_ to store the results of + the {dfanalytics-job}. + + `results_field`::: + (Optional, string) Defines the name of the field in which to store the + results of the analysis. Default to `ml`. +end::dest[] tag::detector-description[] A description of the detector. For example, `Low event rate`. @@ -455,8 +603,7 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=use-null] end::detector[] tag::eta[] -`eta`:: -(Optional, double) The shrinkage applied to the weights. Smaller values result +The shrinkage applied to the weights. Smaller values result in larger forests which have better generalization error. However, the smaller the value the longer the training will take. For more information, see https://en.wikipedia.org/wiki/Gradient_boosting#Shrinkage[this wiki article] @@ -471,11 +618,39 @@ working with both over and by fields, then you can set `exclude_frequent` to `all` for both fields, or to `by` or `over` for those specific fields. end::exclude-frequent[] -tag::feature_bag_fraction[] -`feature_bag_fraction`:: -(Optional, double) Defines the fraction of features that will be used when +tag::feature-bag-fraction[] +Defines the fraction of features that will be used when selecting a random bag for each candidate split. -end::feature_bag_fraction[] +end::feature-bag-fraction[] + +tag::feature-influence-threshold[] +The minimum {olscore} that a document needs to have in order to calculate its +{fiscore}. Value range: 0-1 (`0.1` by default). +end::feature-influence-threshold[] + +tag::field-selection[] +An array of objects that explain selection for each field, sorted by +the field names. Each object in the array has the following properties: + +`name`::: +(string) The field name. + +`mapping_types`::: +(string) The mapping types of the field. + +`is_included`::: +(boolean) Whether the field is selected to be included in the analysis. + +`is_required`::: +(boolean) Whether the field is required. + +`feature_type`::: +(string) The feature type of this field for the analysis. May be `categorical` +or `numerical`. + +`reason`::: +(string) The reason a field is not selected to be included in the analysis. +end::field-selection[] tag::filter[] One or more <>. In addition to the built-in @@ -491,6 +666,10 @@ for longer bucket spans, a sensible fraction of the bucket span. For example: `150s`. end::frequency[] +tag::from[] +Skips the specified number of {dfanalytics-jobs}. The default value is `0`. +end::from[] + tag::function[] The analysis function that is used. For example, `count`, `rare`, `mean`, `min`, `max`, and `sum`. For more information, see @@ -498,8 +677,7 @@ The analysis function that is used. For example, `count`, `rare`, `mean`, `min`, end::function[] tag::gamma[] -`gamma`:: -(Optional, double) Regularization parameter to prevent overfitting on the +Regularization parameter to prevent overfitting on the training dataset. Multiplies a linear penalty associated with the size of individual trees in the forest. The higher the value the more training will prefer smaller trees. The smaller this parameter the larger individual trees @@ -538,7 +716,9 @@ Identifier for the {dfanalytics-job}. end::job-id-data-frame-analytics[] tag::job-id-anomaly-detection-default[] -Identifier for the {anomaly-job}. It can be a job identifier, a group name, or a wildcard expression. If you do not specify one of these options, the API returns information for all {anomaly-jobs}. +Identifier for the {anomaly-job}. It can be a job identifier, a group name, or a +wildcard expression. If you do not specify one of these options, the API returns +information for all {anomaly-jobs}. end::job-id-anomaly-detection-default[] tag::job-id-data-frame-analytics-default[] @@ -552,7 +732,8 @@ identifier, a group name, or a comma-separated list of jobs or groups. end::job-id-anomaly-detection-list[] tag::job-id-anomaly-detection-wildcard[] -Identifier for the {anomaly-job}. It can be a job identifier, a group name, or a wildcard expression. +Identifier for the {anomaly-job}. It can be a job identifier, a group name, or a +wildcard expression. end::job-id-anomaly-detection-wildcard[] tag::job-id-anomaly-detection-wildcard-list[] @@ -561,9 +742,9 @@ comma-separated list of jobs or groups, or a wildcard expression. end::job-id-anomaly-detection-wildcard-list[] tag::job-id-anomaly-detection-define[] -Identifier for the {anomaly-job}. This identifier can contain lowercase alphanumeric -characters (a-z and 0-9), hyphens, and underscores. It must start and end with -alphanumeric characters. +Identifier for the {anomaly-job}. This identifier can contain lowercase +alphanumeric characters (a-z and 0-9), hyphens, and underscores. It must start +and end with alphanumeric characters. end::job-id-anomaly-detection-define[] tag::job-id-data-frame-analytics-define[] @@ -577,8 +758,7 @@ The unique identifier for the job to which the {dfeed} sends data. end::job-id-datafeed[] tag::lambda[] -`lambda`:: -(Optional, double) Regularization parameter to prevent overfitting on the +Regularization parameter to prevent overfitting on the training dataset. Multiplies an L2 regularisation term which applies to leaf weights of the individual trees in the forest. The higher the value the more training will attempt to keep leaf weights small. This makes the prediction @@ -589,7 +769,9 @@ end::lambda[] tag::latency[] The size of the window in which to expect data that is out of time order. The -default value is 0 (no latency). If you specify a non-zero value, it must be greater than or equal to one second. For more information about time units, see <>. +default value is 0 (no latency). If you specify a non-zero value, it must be +greater than or equal to one second. For more information about time units, see +<>. + -- NOTE: Latency is only applicable when you send data by using @@ -607,11 +789,33 @@ If not set then a {dfeed} with no end time that sees no data will remain started until it is explicitly stopped. By default this setting is not set. end::max-empty-searches[] -tag::maximum_number_trees[] -`maximum_number_trees`:: -(Optional, integer) Defines the maximum number of trees the forest is allowed +tag::maximum-number-trees[] +Defines the maximum number of trees the forest is allowed to contain. The maximum value is 2000. -end::maximum_number_trees[] +end::maximum-number-trees[] + +tag::memory-estimation[] +An object containing the memory estimates. The object has the +following properties: + +`expected_memory_without_disk`::: +(string) Estimated memory usage under the assumption that the whole +{dfanalytics} should happen in memory (i.e. without overflowing to disk). + +`expected_memory_with_disk`::: +(string) Estimated memory usage under the assumption that overflowing to disk is +allowed during {dfanalytics}. `expected_memory_with_disk` is usually smaller +than `expected_memory_without_disk` as using disk allows to limit the main +memory needed to perform {dfanalytics}. +end::memory-estimation[] + +tag::method[] +Sets the method that {oldetection} uses. If the method is not set {oldetection} +uses an ensemble of different methods and normalises and combines their +individual {olscores} to obtain the overall {olscore}. We recommend to use the +ensemble method. Available methods are `lof`, `ldof`, `distance_kth_nn`, +`distance_knn`. +end::method[] tag::mode[] There are three available modes: @@ -649,20 +853,30 @@ see <>. -- end::model-memory-limit[] +tag::model-memory-limit-dfa[] +The approximate maximum amount of memory resources that are permitted for +analytical processing. The default value for {dfanalytics-jobs} is `1gb`. If +your `elasticsearch.yml` file contains an `xpack.ml.max_model_memory_limit` +setting, an error occurs when you try to create {dfanalytics-jobs} that have +`model_memory_limit` values greater than that setting. For more information, see +<>. +end::model-memory-limit-dfa[] + tag::model-plot-config[] This advanced configuration option stores model information along with the results. It provides a more detailed view into {anomaly-detect}. + -- -WARNING: If you enable model plot it can add considerable overhead to the performance -of the system; it is not feasible for jobs with many entities. +WARNING: If you enable model plot it can add considerable overhead to the +performance of the system; it is not feasible for jobs with many entities. -Model plot provides a simplified and indicative view of the model and its bounds. -It does not display complex features such as multivariate correlations or multimodal data. -As such, anomalies may occasionally be reported which cannot be seen in the model plot. +Model plot provides a simplified and indicative view of the model and its +bounds. It does not display complex features such as multivariate correlations +or multimodal data. As such, anomalies may occasionally be reported which cannot +be seen in the model plot. -Model plot config can be configured when the job is created or updated later. It must be -disabled if performance issues are experienced. +Model plot config can be configured when the job is created or updated later. It +must be disabled if performance issues are experienced. The `model_plot_config` object has the following properties: @@ -713,6 +927,21 @@ NOTE: To use the `multivariate_by_fields` property, you must also specify -- end::multivariate-by-fields[] +tag::n-neighbors[] +Defines the value for how many nearest neighbors each method of +{oldetection} will use to calculate its {olscore}. When the value is not set, +different values will be used for different ensemble members. This helps +improve diversity in the ensemble. Therefore, only override this if you are +confident that the value you choose is appropriate for the data set. +end::n-neighbors[] + +tag::num-top-classes[] +Defines the number of categories for which the predicted +probabilities are reported. It must be non-negative. If it is greater than the +total number of categories (in the {version} version of the {stack}, it's two) +to predict then we will report all category probabilities. Defaults to 2. +end::num-top-classes[] + tag::over-field-name[] The field used to split the data. In particular, this property is used for analyzing the splits with respect to the history of all splits. It is used for @@ -720,16 +949,29 @@ finding unusual values in the population of all splits. For more information, see {stack-ov}/ml-configuring-pop.html[Performing population analysis]. end::over-field-name[] +tag::outlier-fraction[] +Sets the proportion of the data set that is assumed to be outlying prior to +{oldetection}. For example, 0.05 means it is assumed that 5% of values are real +outliers and 95% are inliers. +end::outlier-fraction[] + tag::partition-field-name[] The field used to segment the analysis. When you use this property, you have completely independent baselines for each value of this field. end::partition-field-name[] -tag::prediction_field_name[] -`prediction_field_name`:: -(Optional, string) Defines the name of the prediction field in the results. +tag::prediction-field-name[] +Defines the name of the prediction field in the results. Defaults to `_prediction`. -end::prediction_field_name[] +end::prediction-field-name[] + +tag::randomize-seed[] +Defines the seed to the random generator that is used to pick which documents +will be used for training. By default it is randomly generated. Set it to a +specific value to ensure the same documents are used for training assuming other +related parameters (for example, `source`, `analyzed_fields`, etc.) are the +same. +end::randomize-seed[] tag::query[] The {es} query domain-specific language (DSL). This value corresponds to the @@ -776,6 +1018,48 @@ tag::scroll-size[] The `size` parameter that is used in {es} searches. The default value is `1000`. end::scroll-size[] +tag::size[] +Specifies the maximum number of {dfanalytics-jobs} to obtain. The default value +is `100`. +end::size[] + +tag::source-put-dfa[] +The configuration of how to source the analysis data. It requires an +`index`. Optionally, `query` and `_source` may be specified. + +`index`::: + (Required, string or array) Index or indices on which to perform the + analysis. It can be a single index or index pattern as well as an array of + indices or patterns. + +`query`::: + (Optional, object) The {es} query domain-specific language + (<>). This value corresponds to the query object in an {es} + search POST body. All the options that are supported by {es} can be used, + as this object is passed verbatim to {es}. By default, this property has + the following value: `{"match_all": {}}`. + +`_source`::: + (Optional, object) Specify `includes` and/or `excludes` patterns to select + which fields will be present in the destination. Fields that are excluded + cannot be included in the analysis. + + `includes`:::: + (array) An array of strings that defines the fields that will be + included in the destination. + + `excludes`:::: + (array) An array of strings that defines the fields that will be + excluded from the destination. +end::source-put-dfa[] + +tag::standardization-enabled[] +If `true`, then the following operation is performed on the columns before +computing outlier scores: (x_i - mean(x_i)) / sd(x_i). Defaults to `true`. For +more information, see +https://en.wikipedia.org/wiki/Feature_scaling#Standardization_(Z-score_Normalization)[this wiki page about standardization]. +end::standardization-enabled[] + tag::summary-count-field-name[] If this property is specified, the data that is fed to the job is expected to be pre-summarized. This property value is the name of the field that contains the @@ -789,6 +1073,16 @@ function. -- end::summary-count-field-name[] +tag::timeout-start[] +Controls the amount of time to wait until the {dfanalytics-job} starts. Defaults +to 20 seconds. +end::timeout-start[] + +tag::timeout-stop[] +Controls the amount of time to wait until the {dfanalytics-job} stops. Defaults +to 20 seconds. +end::timeout-stop[] + tag::time-format[] The time format, which can be `epoch`, `epoch_ms`, or a custom pattern. The default value is `epoch`, which refers to UNIX or Epoch time (the number of @@ -800,8 +1094,8 @@ either integer or real values. + NOTE: Custom patterns must conform to the Java `DateTimeFormatter` class. When you use date-time formatting patterns, it is recommended that you provide the full date, time and time zone. For example: `yyyy-MM-dd'T'HH:mm:ssX`. -If the pattern that you specify is not sufficient to produce a complete timestamp, -job creation fails. +If the pattern that you specify is not sufficient to produce a complete +timestamp, job creation fails. -- end::time-format[] @@ -821,22 +1115,12 @@ that tokenizer but change the character or token filters, specify `"tokenizer": "ml_classic"` in your `categorization_analyzer`. end::tokenizer[] -tag::training_percent[] -`training_percent`:: -(Optional, integer) Defines what percentage of the eligible documents that will +tag::training-percent[] +Defines what percentage of the eligible documents that will be used for training. Documents that are ignored by the analysis (for example those that contain arrays) won’t be included in the calculation for used percentage. Defaults to `100`. -end::training_percent[] - -tag::randomize_seed[] -`randomize_seed`:: -(Optional, long) Defines the seed to the random generator that is used to pick -which documents will be used for training. By default it is randomly generated. -Set it to a specific value to ensure the same documents are used for training -assuming other related parameters (e.g. `source`, `analyzed_fields`, etc.) are the same. -end::randomize_seed[] - +end::training-percent[] tag::use-null[] Defines whether a new series is used as the null series when there is no value diff --git a/docs/reference/rest-api/defs.asciidoc b/docs/reference/rest-api/defs.asciidoc index 0ef152e917f8e..312a889707110 100644 --- a/docs/reference/rest-api/defs.asciidoc +++ b/docs/reference/rest-api/defs.asciidoc @@ -5,15 +5,15 @@ These resource definitions are used in APIs related to {ml-features} and {security-features} and in {kib} advanced {ml} job configuration options. -* <> -* <> + +* <> * <> * <> * <> * <> -include::{es-repo-dir}/ml/df-analytics/apis/dfanalyticsresources.asciidoc[] -include::{es-repo-dir}/ml/df-analytics/apis/evaluateresources.asciidoc[] + +include::{es-repo-dir}/ml/df-analytics/apis/analysisobjects.asciidoc[] include::{es-repo-dir}/ml/anomaly-detection/apis/snapshotresource.asciidoc[] include::{xes-repo-dir}/rest-api/security/role-mapping-resources.asciidoc[] include::{es-repo-dir}/ml/anomaly-detection/apis/resultsresource.asciidoc[] From cc4af53b053c99fd6c1a66d341e57fe2d0efb2a4 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Thu, 12 Dec 2019 11:03:59 +0000 Subject: [PATCH 173/686] Enable trace logging in failing ml NetworkDisruptionIT https://github.com/elastic/elasticsearch/issues/49908 --- .../xpack/ml/integration/NetworkDisruptionIT.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/NetworkDisruptionIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/NetworkDisruptionIT.java index 8a257baa3d628..379daba7c45ea 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/NetworkDisruptionIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/NetworkDisruptionIT.java @@ -13,6 +13,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.disruption.NetworkDisruption; +import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.test.transport.MockTransportService; import org.elasticsearch.xpack.core.ml.action.CloseJobAction; import org.elasticsearch.xpack.core.ml.action.OpenJobAction; @@ -38,6 +39,8 @@ protected Collection> nodePlugins() { return plugins; } + @TestLogging(value = "org.elasticsearch.persistent.PersistentTasksClusterService:trace", + reason = "https://github.com/elastic/elasticsearch/issues/49908") public void testJobRelocation() throws Exception { internalCluster().ensureAtLeastNumDataNodes(5); ensureStableCluster(5); From 745ef3e3cde9d5f3bae482d75ce53b09b7a4fe8f Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 12 Dec 2019 12:17:44 +0100 Subject: [PATCH 174/686] upgrade to lucene 8.4.0-snapshot-08b8d116f8f (#50129) --- buildSrc/version.properties | 2 +- .../lucene-expressions-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../licenses/lucene-expressions-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../lucene-analyzers-icu-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../lucene-analyzers-icu-8.4.0-snapshot-662c455.jar.sha1 | 1 - ...ucene-analyzers-kuromoji-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../lucene-analyzers-kuromoji-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../lucene-analyzers-nori-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../lucene-analyzers-nori-8.4.0-snapshot-662c455.jar.sha1 | 1 - ...ucene-analyzers-phonetic-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../lucene-analyzers-phonetic-8.4.0-snapshot-662c455.jar.sha1 | 1 - ...lucene-analyzers-smartcn-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../lucene-analyzers-smartcn-8.4.0-snapshot-662c455.jar.sha1 | 1 - ...lucene-analyzers-stempel-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../lucene-analyzers-stempel-8.4.0-snapshot-662c455.jar.sha1 | 1 - ...ene-analyzers-morfologik-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../lucene-analyzers-morfologik-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../lucene-analyzers-common-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../lucene-analyzers-common-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../lucene-backward-codecs-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../lucene-backward-codecs-8.4.0-snapshot-662c455.jar.sha1 | 1 - server/licenses/lucene-core-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + server/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../lucene-grouping-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + server/licenses/lucene-grouping-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../lucene-highlighter-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../licenses/lucene-highlighter-8.4.0-snapshot-662c455.jar.sha1 | 1 - server/licenses/lucene-join-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + server/licenses/lucene-join-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../licenses/lucene-memory-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + server/licenses/lucene-memory-8.4.0-snapshot-662c455.jar.sha1 | 1 - server/licenses/lucene-misc-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + server/licenses/lucene-misc-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../licenses/lucene-queries-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + server/licenses/lucene-queries-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../lucene-queryparser-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../licenses/lucene-queryparser-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../licenses/lucene-sandbox-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + server/licenses/lucene-sandbox-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../licenses/lucene-spatial-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + server/licenses/lucene-spatial-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../lucene-spatial-extras-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../lucene-spatial-extras-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../lucene-spatial3d-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../licenses/lucene-spatial3d-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../licenses/lucene-suggest-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + server/licenses/lucene-suggest-8.4.0-snapshot-662c455.jar.sha1 | 1 - .../licenses/lucene-core-8.4.0-snapshot-08b8d116f8f.jar.sha1 | 1 + .../licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 | 1 - 49 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-analyzers-common-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-analyzers-common-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-backward-codecs-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-backward-codecs-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-core-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-grouping-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-grouping-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-highlighter-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-highlighter-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-join-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-join-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-memory-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-memory-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-misc-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-misc-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-queries-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-queries-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-queryparser-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-queryparser-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-sandbox-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-sandbox-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-spatial-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-spatial-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-spatial-extras-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-spatial-extras-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-spatial3d-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-spatial3d-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 server/licenses/lucene-suggest-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 server/licenses/lucene-suggest-8.4.0-snapshot-662c455.jar.sha1 create mode 100644 x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-08b8d116f8f.jar.sha1 delete mode 100644 x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 diff --git a/buildSrc/version.properties b/buildSrc/version.properties index ad486276f082e..c7e957939fc55 100644 --- a/buildSrc/version.properties +++ b/buildSrc/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.0.0 -lucene = 8.4.0-snapshot-662c455 +lucene = 8.4.0-snapshot-08b8d116f8f bundled_jdk_vendor = adoptopenjdk bundled_jdk = 13.0.1+9 diff --git a/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..0f2b32ad7ef61 --- /dev/null +++ b/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +a02eaef7b91b383f7ca43f4a33726bd089c0a36e \ No newline at end of file diff --git a/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-662c455.jar.sha1 b/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 1c4c5ce2b62db..0000000000000 --- a/modules/lang-expression/licenses/lucene-expressions-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -4041db9db7c394584571b45812734732912ef8e2 \ No newline at end of file diff --git a/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..058b07b9dba7d --- /dev/null +++ b/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +7c7054c935661dde19afeb0b9c7b8bda65c0dafe \ No newline at end of file diff --git a/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 0fc96bc500eff..0000000000000 --- a/plugins/analysis-icu/licenses/lucene-analyzers-icu-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -d5bddd6b7660439e29bbce26ded283931c756d75 \ No newline at end of file diff --git a/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..decd6216aeece --- /dev/null +++ b/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +4dc8d6f78242fc384fd03a2a2e93d1737b0d3c11 \ No newline at end of file diff --git a/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 388bc9748b7f0..0000000000000 --- a/plugins/analysis-kuromoji/licenses/lucene-analyzers-kuromoji-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -4303858c346c51bbbc68c32eb25f7f372b09331c \ No newline at end of file diff --git a/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..3c34ab4664b7c --- /dev/null +++ b/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +40baaa81cb2b986d086b0ebc648415da84c15552 \ No newline at end of file diff --git a/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 07ff7fd907a22..0000000000000 --- a/plugins/analysis-nori/licenses/lucene-analyzers-nori-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -b1a9182ed1b92a121c1587fe9710aa7a41f3f77a \ No newline at end of file diff --git a/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..623f9f1aefd26 --- /dev/null +++ b/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +8739dcdb8ca9fdebb93f6e696626119d30d5cb58 \ No newline at end of file diff --git a/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 95e603ec18882..0000000000000 --- a/plugins/analysis-phonetic/licenses/lucene-analyzers-phonetic-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -4df747b25286baecf5e790bf76bc40038c059691 \ No newline at end of file diff --git a/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..4b4d838d03fbf --- /dev/null +++ b/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +74a27bf3c523156fadc0159cd1311388505d6cae \ No newline at end of file diff --git a/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 4eaf91f308397..0000000000000 --- a/plugins/analysis-smartcn/licenses/lucene-analyzers-smartcn-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -88d3f8f9134b95884f3b80280b09aa2513b71297 \ No newline at end of file diff --git a/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..f5510f6f731ee --- /dev/null +++ b/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +374d411635868394719f4d328a26dbc6a9b0eded \ No newline at end of file diff --git a/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index e28b8d87cd559..0000000000000 --- a/plugins/analysis-stempel/licenses/lucene-analyzers-stempel-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -9ddccf575ee03a1329c8d1eb2e4ee7a6e3f3f56f \ No newline at end of file diff --git a/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..5c9510b012765 --- /dev/null +++ b/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +1cf79c772b6cfecc80137bcde48b7a5432ee5028 \ No newline at end of file diff --git a/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-662c455.jar.sha1 b/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 1b8ec8c5831cb..0000000000000 --- a/plugins/analysis-ukrainian/licenses/lucene-analyzers-morfologik-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -e115e562a42c12a3292fb138607855c1fdfb0772 \ No newline at end of file diff --git a/server/licenses/lucene-analyzers-common-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-analyzers-common-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..03841938296d2 --- /dev/null +++ b/server/licenses/lucene-analyzers-common-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +dc14a70f44c2597e237f37f096d6b8c2db8cd335 \ No newline at end of file diff --git a/server/licenses/lucene-analyzers-common-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-analyzers-common-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index d6f8049f7b1e1..0000000000000 --- a/server/licenses/lucene-analyzers-common-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -061fb94ab616492721f8868dcaec3fbc989733be \ No newline at end of file diff --git a/server/licenses/lucene-backward-codecs-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-backward-codecs-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..76fbf985dc8c8 --- /dev/null +++ b/server/licenses/lucene-backward-codecs-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +535e6d650fe66ef8df9fd03ad48170f6c0e7c4cf \ No newline at end of file diff --git a/server/licenses/lucene-backward-codecs-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-backward-codecs-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 243c4420beabe..0000000000000 --- a/server/licenses/lucene-backward-codecs-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -503f3d516889a99e1c0e2dbdba7bf9cc9900c54c \ No newline at end of file diff --git a/server/licenses/lucene-core-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-core-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..cb9ce3f8db761 --- /dev/null +++ b/server/licenses/lucene-core-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +2d20a2d0f96f4337665b3043e627df6ce83ed41c \ No newline at end of file diff --git a/server/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index d1657fccc5ee2..0000000000000 --- a/server/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -8ca36adea0a904ec725d57f509a62652a53ecff8 \ No newline at end of file diff --git a/server/licenses/lucene-grouping-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-grouping-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..10b0df8788174 --- /dev/null +++ b/server/licenses/lucene-grouping-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +cb9218eb5a5c9ef84377f9533e8a2d13c55ed71b \ No newline at end of file diff --git a/server/licenses/lucene-grouping-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-grouping-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index f1f0684d9b389..0000000000000 --- a/server/licenses/lucene-grouping-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -f176fdcf8fc574f4cb1c549aaa4da0301afd34ba \ No newline at end of file diff --git a/server/licenses/lucene-highlighter-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-highlighter-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..9ae147d34b61a --- /dev/null +++ b/server/licenses/lucene-highlighter-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +0cbb63e519f3e0acf7bdbc6dcd6c2bc4bb003b70 \ No newline at end of file diff --git a/server/licenses/lucene-highlighter-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-highlighter-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index a9ad6fb95cb8b..0000000000000 --- a/server/licenses/lucene-highlighter-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -db5ea7b647309e5d29fa92bcbb6b11286d11436d \ No newline at end of file diff --git a/server/licenses/lucene-join-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-join-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..59cb050fe465f --- /dev/null +++ b/server/licenses/lucene-join-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +0f843b878752449f345d6fd5048f62b1c0688fca \ No newline at end of file diff --git a/server/licenses/lucene-join-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-join-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 6ef1d079f63f1..0000000000000 --- a/server/licenses/lucene-join-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -36329bc2ea6a5640d4128206221456656de7bbe2 \ No newline at end of file diff --git a/server/licenses/lucene-memory-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-memory-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..78951b539c098 --- /dev/null +++ b/server/licenses/lucene-memory-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +e5583564c96d24d7293574efbd5d55d5e5d55f58 \ No newline at end of file diff --git a/server/licenses/lucene-memory-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-memory-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index eeb424851022e..0000000000000 --- a/server/licenses/lucene-memory-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -083f492781b3d2c1d470bd1439c875ebf74a14eb \ No newline at end of file diff --git a/server/licenses/lucene-misc-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-misc-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..86a36ef22e4b5 --- /dev/null +++ b/server/licenses/lucene-misc-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +1a20042a3c79e86b0c95f7bc19bdb835e4fba74e \ No newline at end of file diff --git a/server/licenses/lucene-misc-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-misc-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 6f5d479c76d64..0000000000000 --- a/server/licenses/lucene-misc-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -9cd5ea7bc08d93053ca993bd6fc1c9cd0a1b91fd \ No newline at end of file diff --git a/server/licenses/lucene-queries-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-queries-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..8e7766918327b --- /dev/null +++ b/server/licenses/lucene-queries-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +60f1ba706bde727fcbb333e8bc52d0cf53daa776 \ No newline at end of file diff --git a/server/licenses/lucene-queries-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-queries-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 30733a5a57764..0000000000000 --- a/server/licenses/lucene-queries-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -89e39f65d1c42b5849ccf3a8e6cc9b3b277c08a6 \ No newline at end of file diff --git a/server/licenses/lucene-queryparser-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-queryparser-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..eec0b037dfebd --- /dev/null +++ b/server/licenses/lucene-queryparser-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +a99fbcf01d2ee081b04ed7e173c71bcc1f2c4a83 \ No newline at end of file diff --git a/server/licenses/lucene-queryparser-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-queryparser-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 98b065176a418..0000000000000 --- a/server/licenses/lucene-queryparser-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -651f6a0075ee30b814c8b56020d95155424c0e67 \ No newline at end of file diff --git a/server/licenses/lucene-sandbox-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-sandbox-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..66881bedef5da --- /dev/null +++ b/server/licenses/lucene-sandbox-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +613057333e965ada682126b7e97d8d7fb6ab7db8 \ No newline at end of file diff --git a/server/licenses/lucene-sandbox-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-sandbox-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 484ce6b5c00f0..0000000000000 --- a/server/licenses/lucene-sandbox-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -935968488cc2bbcd3ced9c254f690e7c90447d9e \ No newline at end of file diff --git a/server/licenses/lucene-spatial-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-spatial-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..6b7f37fa76630 --- /dev/null +++ b/server/licenses/lucene-spatial-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +2727fc11cb9d503388a8fb09159de8db17772de9 \ No newline at end of file diff --git a/server/licenses/lucene-spatial-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-spatial-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 1bb42417cb147..0000000000000 --- a/server/licenses/lucene-spatial-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -0bbdd0002d8d87e54b5caff6c77a1627bf449d38 \ No newline at end of file diff --git a/server/licenses/lucene-spatial-extras-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-spatial-extras-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..4c480aff42ae6 --- /dev/null +++ b/server/licenses/lucene-spatial-extras-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +7bd96ac0db2bf246e8a5636409201fa1ca1619fe \ No newline at end of file diff --git a/server/licenses/lucene-spatial-extras-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-spatial-extras-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 2bdbd889b4454..0000000000000 --- a/server/licenses/lucene-spatial-extras-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -255b547571dcec118ff1a0560bb16e259f96b76a \ No newline at end of file diff --git a/server/licenses/lucene-spatial3d-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-spatial3d-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..1bb87cc3f586c --- /dev/null +++ b/server/licenses/lucene-spatial3d-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +f4c57666b25335ff797c76932563b3f4e33f138d \ No newline at end of file diff --git a/server/licenses/lucene-spatial3d-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-spatial3d-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index e7036243119a9..0000000000000 --- a/server/licenses/lucene-spatial3d-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -739af6d9876f6aa7f2a3d46fa3f236a5d6ee3653 \ No newline at end of file diff --git a/server/licenses/lucene-suggest-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/server/licenses/lucene-suggest-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..31d4b678e0536 --- /dev/null +++ b/server/licenses/lucene-suggest-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +2351b9abaa5fed697217853db44c038070660b75 \ No newline at end of file diff --git a/server/licenses/lucene-suggest-8.4.0-snapshot-662c455.jar.sha1 b/server/licenses/lucene-suggest-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index 72c92c101b050..0000000000000 --- a/server/licenses/lucene-suggest-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -20fa11a541a7ca3a50caa443a9abf0276b1194ea \ No newline at end of file diff --git a/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-08b8d116f8f.jar.sha1 b/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-08b8d116f8f.jar.sha1 new file mode 100644 index 0000000000000..cb9ce3f8db761 --- /dev/null +++ b/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-08b8d116f8f.jar.sha1 @@ -0,0 +1 @@ +2d20a2d0f96f4337665b3043e627df6ce83ed41c \ No newline at end of file diff --git a/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 b/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 deleted file mode 100644 index d1657fccc5ee2..0000000000000 --- a/x-pack/plugin/sql/sql-action/licenses/lucene-core-8.4.0-snapshot-662c455.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -8ca36adea0a904ec725d57f509a62652a53ecff8 \ No newline at end of file From ba6b50249978f5ab92a824f6be5c81c1e8bac935 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 12 Dec 2019 13:12:04 +0100 Subject: [PATCH 175/686] cat.indices.json bytes enum not exhaustive (#49369) Missing several valid options that other cat API's do define --- .../main/resources/rest-api-spec/api/cat.indices.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.indices.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.indices.json index 60a98412c31b3..68194bc652b29 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.indices.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.indices.json @@ -38,8 +38,15 @@ "options":[ "b", "k", + "kb", "m", - "g" + "mb", + "g", + "gb", + "t", + "tb", + "p", + "pb" ] }, "local":{ From 09f1903c832bea47fd7de85d493884aaa535fc9a Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 12 Dec 2019 13:16:32 +0100 Subject: [PATCH 176/686] Remove Unused Delete Endpoint from GCS Mock (#50128) Follow up to #50024: we're not using the single-delete any more so no need to have a mock endpoint for it --- .../fixture/gcs/GoogleCloudStorageHttpHandler.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java index 967c884104c96..1b4c6b4caacb9 100644 --- a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java +++ b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java @@ -43,7 +43,6 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -145,18 +144,6 @@ public void handle(final HttpExchange exchange) throws IOException { exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1); } - } else if (Regex.simpleMatch("DELETE /storage/v1/b/" + bucket + "/o/*", request)) { - // Delete Object https://cloud.google.com/storage/docs/json_api/v1/objects/delete - int deletions = 0; - for (Iterator> iterator = blobs.entrySet().iterator(); iterator.hasNext(); ) { - Map.Entry blob = iterator.next(); - if (blob.getKey().equals(exchange.getRequestURI().toString())) { - iterator.remove(); - deletions++; - } - } - exchange.sendResponseHeaders((deletions > 0 ? RestStatus.OK : RestStatus.NO_CONTENT).getStatus(), -1); - } else if (Regex.simpleMatch("POST /batch/storage/v1", request)) { // Batch https://cloud.google.com/storage/docs/json_api/v1/how-tos/batch final String uri = "/storage/v1/b/" + bucket + "/o/"; From 8d9190682430bad03cf3e042b67a25f727b7d30b Mon Sep 17 00:00:00 2001 From: Tomas Della Vedova Date: Thu, 12 Dec 2019 13:56:16 +0100 Subject: [PATCH 177/686] Added type definitions in rest-api-spec (#47089) * Added type definitions in rest-api-spec * Addressed comments --- rest-api-spec/README.markdown | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/rest-api-spec/README.markdown b/rest-api-spec/README.markdown index 8092546fd5a30..9bbef355b3bd6 100644 --- a/rest-api-spec/README.markdown +++ b/rest-api-spec/README.markdown @@ -67,6 +67,23 @@ If an API is stable but it response should be treated as an arbitrary map of key } ``` +## Type definition +In the documentation, you will find the `type` field, which documents which type every parameter will accept. + +#### Querystring parameters +| Type | Description | +|---|---| +| `list` | An array of strings *(represented as a comma separated list in the querystring)* | +| `date` | A string representing a date formatted in ISO8601 or a number representing milliseconds since the epoch *(used only in ML)* | +| `time` | A numeric or string value representing duration | +| `string` | A string value | +| `enum` | A set of named constants *(a single value should be sent in the querystring)* | +| `int` | A signed 32-bit integer with a minimum value of -231 and a maximum value of 231-1. | +| `double` | A [double-precision 64-bit IEEE 754](https://en.wikipedia.org/wiki/Floating-point_arithmetic) floating point number, restricted to finite values. | +| `long` | A signed 64-bit integer with a minimum value of -263 and a maximum value of 263-1. *(Note: the max safe integer for JSON is 253-1)* | +| `number` | Alias for `double`. *(deprecated, a more specific type should be used)* | +| `boolean` | Boolean fields accept JSON true and false values | + ## Backwards compatibility The specification follows the same backward compatibility guarantees as Elasticsearch. From 9008cf3b5da756c131daf8ff16a14c1495f510a4 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Thu, 12 Dec 2019 08:38:48 -0500 Subject: [PATCH 178/686] [DOCS] Correct percentile rank agg example response (#50052) The example snippets in the percentile rank agg docs use a test dataset named `latency`, which is generated from docs/gradle.build. At some point the dataset and example snippets were updated, but the text surrounding the snippets was not. This means the text and the example snippets shown no longer match up. This corrects that by changing the snippets using /TESTRESPONSE magic comments. --- .../metrics/percentile-rank-aggregation.asciidoc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/reference/aggregations/metrics/percentile-rank-aggregation.asciidoc b/docs/reference/aggregations/metrics/percentile-rank-aggregation.asciidoc index d0765ea026846..71bc5ceee555d 100644 --- a/docs/reference/aggregations/metrics/percentile-rank-aggregation.asciidoc +++ b/docs/reference/aggregations/metrics/percentile-rank-aggregation.asciidoc @@ -51,14 +51,16 @@ The response will look like this: "aggregations": { "load_time_ranks": { "values" : { - "500.0": 55.00000000000001, - "600.0": 64.0 + "500.0": 90.01, + "600.0": 100.0 } } } } -------------------------------------------------- // TESTRESPONSE[s/\.\.\./"took": $body.took,"timed_out": false,"_shards": $body._shards,"hits": $body.hits,/] +// TESTRESPONSE[s/"500.0": 90.01/"500.0": 55.00000000000001/] +// TESTRESPONSE[s/"600.0": 100.0/"600.0": 64.0/] From this information you can determine you are hitting the 99% load time target but not quite hitting the 95% load time target @@ -97,11 +99,11 @@ Response: "values": [ { "key": 500.0, - "value": 55.00000000000001 + "value": 90.01 }, { "key": 600.0, - "value": 64.0 + "value": 100.0 } ] } @@ -109,6 +111,8 @@ Response: } -------------------------------------------------- // TESTRESPONSE[s/\.\.\./"took": $body.took,"timed_out": false,"_shards": $body._shards,"hits": $body.hits,/] +// TESTRESPONSE[s/"value": 90.01/"value": 55.00000000000001/] +// TESTRESPONSE[s/"value": 100.0/"value": 64.0/] ==== Script From 2f6a6b118956628f002da188ea0decb90487ce76 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Thu, 12 Dec 2019 09:03:33 -0500 Subject: [PATCH 179/686] [ML] Add graceful retry for anomaly detector result indexing failures (#49508) All results indexing now retry the amount of times configured in `xpack.ml.persist_results_max_retries`. The retries are done in a semi-random, exponential backoff. --- .../ml/integration/BulkFailureRetryIT.java | 214 ++++++++++++++++++ .../xpack/ml/MachineLearning.java | 11 +- .../TransportRevertModelSnapshotAction.java | 2 +- .../persistence/JobDataCountsPersister.java | 51 ++++- .../job/persistence/JobResultsPersister.java | 82 ++++--- .../ml/job/process/DataCountsReporter.java | 5 +- .../output/AutodetectResultProcessor.java | 29 ++- .../writer/CsvDataToProcessWriter.java | 8 +- .../writer/JsonDataToProcessWriter.java | 11 +- .../persistence/ResultsPersisterService.java | 173 ++++++++++++++ .../AutodetectResultProcessorIT.java | 24 +- .../ml/integration/EstablishedMemUsageIT.java | 31 ++- .../ml/integration/JobResultsProviderIT.java | 62 ++--- .../persistence/JobResultsPersisterTests.java | 112 +++++---- .../job/process/DataCountsReporterTests.java | 21 +- .../AutodetectCommunicatorTests.java | 7 +- .../AutodetectProcessManagerTests.java | 8 +- .../AutodetectResultProcessorTests.java | 91 ++++---- .../writer/CsvDataToProcessWriterTests.java | 20 +- .../writer/JsonDataToProcessWriterTests.java | 18 +- .../ResultsPersisterServiceTests.java | 203 +++++++++++++++++ 21 files changed, 960 insertions(+), 223 deletions(-) create mode 100644 x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BulkFailureRetryIT.java create mode 100644 x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/ResultsPersisterService.java create mode 100644 x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/utils/persistence/ResultsPersisterServiceTests.java diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BulkFailureRetryIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BulkFailureRetryIT.java new file mode 100644 index 0000000000000..6dcf306b04a39 --- /dev/null +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BulkFailureRetryIT.java @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.ml.integration; + +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequestBuilder; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.xpack.core.action.util.PageParams; +import org.elasticsearch.xpack.core.ml.action.GetBucketsAction; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; +import org.elasticsearch.xpack.core.ml.job.config.AnalysisConfig; +import org.elasticsearch.xpack.core.ml.job.config.DataDescription; +import org.elasticsearch.xpack.core.ml.job.config.Detector; +import org.elasticsearch.xpack.core.ml.job.config.Job; +import org.elasticsearch.xpack.core.ml.job.results.Bucket; +import org.elasticsearch.xpack.core.ml.job.results.Result; +import org.junit.After; +import org.junit.Before; + +import java.time.Duration; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.elasticsearch.xpack.ml.support.BaseMlIntegTestCase.createDatafeedBuilder; +import static org.hamcrest.Matchers.greaterThan; + +public class BulkFailureRetryIT extends MlNativeAutodetectIntegTestCase { + + private final String index = "bulk-failure-retry"; + private long now = System.currentTimeMillis(); + private static long DAY = Duration.ofDays(1).toMillis(); + private final String jobId = "bulk-failure-retry-job"; + private final String resultsIndex = ".ml-anomalies-custom-bulk-failure-retry-job"; + + @Before + public void putPastDataIntoIndex() { + client().admin().indices().prepareCreate(index) + .addMapping("type", "time", "type=date", "value", "type=long") + .get(); + long twoDaysAgo = now - DAY * 2; + long threeDaysAgo = now - DAY * 3; + writeData(logger, index, 250, threeDaysAgo, twoDaysAgo); + } + + @After + public void cleanUpTest() { + client().admin() + .cluster() + .prepareUpdateSettings() + .setTransientSettings(Settings.builder() + .putNull("xpack.ml.persist_results_max_retries") + .putNull("logger.org.elasticsearch.xpack.ml.datafeed.DatafeedJob") + .putNull("logger.org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister") + .putNull("logger.org.elasticsearch.xpack.ml.job.process.autodetect.output") + .build()).get(); + cleanUp(); + } + + private void ensureAnomaliesWrite() throws InterruptedException { + Settings settings = Settings.builder().put(IndexMetaData.INDEX_READ_ONLY_SETTING.getKey(), false).build(); + AtomicReference acknowledgedResponseHolder = new AtomicReference<>(); + AtomicReference exceptionHolder = new AtomicReference<>(); + blockingCall( + listener -> client().admin().indices().prepareUpdateSettings(resultsIndex).setSettings(settings).execute(listener), + acknowledgedResponseHolder, + exceptionHolder); + if (exceptionHolder.get() != null) { + fail("FAILED TO MARK ["+ resultsIndex + "] as read-write again" + exceptionHolder.get()); + } + } + + private void setAnomaliesReadOnlyBlock() throws InterruptedException { + Settings settings = Settings.builder().put(IndexMetaData.INDEX_READ_ONLY_SETTING.getKey(), true).build(); + AtomicReference acknowledgedResponseHolder = new AtomicReference<>(); + AtomicReference exceptionHolder = new AtomicReference<>(); + blockingCall( + listener -> client().admin().indices().prepareUpdateSettings(resultsIndex).setSettings(settings).execute(listener), + acknowledgedResponseHolder, + exceptionHolder); + if (exceptionHolder.get() != null) { + fail("FAILED TO MARK ["+ resultsIndex + "] as read-ONLY: " + exceptionHolder.get()); + } + } + + public void testBulkFailureRetries() throws Exception { + Job.Builder job = createJob(jobId, TimeValue.timeValueMinutes(5), "count", null); + job.setResultsIndexName(jobId); + + DatafeedConfig.Builder datafeedConfigBuilder = + createDatafeedBuilder(job.getId() + "-datafeed", job.getId(), Collections.singletonList(index)); + DatafeedConfig datafeedConfig = datafeedConfigBuilder.build(); + registerJob(job); + putJob(job); + openJob(job.getId()); + registerDatafeed(datafeedConfig); + putDatafeed(datafeedConfig); + long twoDaysAgo = now - 2 * DAY; + startDatafeed(datafeedConfig.getId(), 0L, twoDaysAgo); + waitUntilJobIsClosed(jobId); + + // Get the job stats + Bucket initialLatestBucket = getLatestFinalizedBucket(jobId); + assertThat(initialLatestBucket.getEpoch(), greaterThan(0L)); + + client().admin() + .cluster() + .prepareUpdateSettings() + .setTransientSettings(Settings.builder() + .put("logger.org.elasticsearch.xpack.ml.datafeed.DatafeedJob", "TRACE") + .put("logger.org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister", "TRACE") + .put("logger.org.elasticsearch.xpack.ml.job.process.autodetect.output", "TRACE") + .put("xpack.ml.persist_results_max_retries", "15") + .build()).get(); + + setAnomaliesReadOnlyBlock(); + + int moreDocs = 1_000; + writeData(logger, index, moreDocs, twoDaysAgo, now); + + openJob(job.getId()); + startDatafeed(datafeedConfig.getId(), twoDaysAgo, now); + + ensureAnomaliesWrite(); + waitUntilJobIsClosed(jobId); + + Bucket newLatestBucket = getLatestFinalizedBucket(jobId); + assertThat(newLatestBucket.getEpoch(), greaterThan(initialLatestBucket.getEpoch())); + } + + private Job.Builder createJob(String id, TimeValue bucketSpan, String function, String field) { + return createJob(id, bucketSpan, function, field, null); + } + + private Job.Builder createJob(String id, TimeValue bucketSpan, String function, String field, String summaryCountField) { + DataDescription.Builder dataDescription = new DataDescription.Builder(); + dataDescription.setFormat(DataDescription.DataFormat.XCONTENT); + dataDescription.setTimeField("time"); + dataDescription.setTimeFormat(DataDescription.EPOCH_MS); + + Detector.Builder d = new Detector.Builder(function, field); + AnalysisConfig.Builder analysisConfig = new AnalysisConfig.Builder(Collections.singletonList(d.build())) + .setBucketSpan(bucketSpan) + .setSummaryCountFieldName(summaryCountField); + + return new Job.Builder().setId(id).setAnalysisConfig(analysisConfig).setDataDescription(dataDescription); + } + + private void writeData(Logger logger, String index, long numDocs, long start, long end) { + int maxDelta = (int) (end - start - 1); + BulkRequestBuilder bulkRequestBuilder = client().prepareBulk(); + for (int i = 0; i < numDocs; i++) { + IndexRequest indexRequest = new IndexRequest(index); + long timestamp = start + randomIntBetween(0, maxDelta); + assert timestamp >= start && timestamp < end; + indexRequest.source("time", timestamp, "value", i); + bulkRequestBuilder.add(indexRequest); + } + BulkResponse bulkResponse = bulkRequestBuilder + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .get(); + if (bulkResponse.hasFailures()) { + int failures = 0; + for (BulkItemResponse itemResponse : bulkResponse) { + if (itemResponse.isFailed()) { + failures++; + logger.error("Item response failure [{}]", itemResponse.getFailureMessage()); + } + } + fail("Bulk response contained " + failures + " failures"); + } + logger.info("Indexed [{}] documents", numDocs); + } + + private Bucket getLatestFinalizedBucket(String jobId) { + GetBucketsAction.Request getBucketsRequest = new GetBucketsAction.Request(jobId); + getBucketsRequest.setExcludeInterim(true); + getBucketsRequest.setSort(Result.TIMESTAMP.getPreferredName()); + getBucketsRequest.setDescending(true); + getBucketsRequest.setPageParams(new PageParams(0, 1)); + return getBuckets(getBucketsRequest).get(0); + } + + private void blockingCall(Consumer> function, + AtomicReference response, + AtomicReference error) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + ActionListener listener = ActionListener.wrap( + r -> { + response.set(r); + latch.countDown(); + }, + e -> { + error.set(e); + latch.countDown(); + } + ); + + function.accept(listener); + latch.await(); + } +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java index 76442812ef256..a7b21bbd721ad 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java @@ -298,6 +298,7 @@ import org.elasticsearch.xpack.ml.rest.results.RestGetRecordsAction; import org.elasticsearch.xpack.ml.rest.validate.RestValidateDetectorAction; import org.elasticsearch.xpack.ml.rest.validate.RestValidateJobConfigAction; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import java.io.IOException; import java.math.BigInteger; @@ -446,7 +447,8 @@ public List> getSettings() { MlConfigMigrationEligibilityCheck.ENABLE_CONFIG_MIGRATION, InferenceProcessor.MAX_INFERENCE_PROCESSORS, ModelLoadingService.INFERENCE_MODEL_CACHE_SIZE, - ModelLoadingService.INFERENCE_MODEL_CACHE_TTL + ModelLoadingService.INFERENCE_MODEL_CACHE_TTL, + ResultsPersisterService.PERSIST_RESULTS_MAX_RETRIES ); } @@ -520,9 +522,12 @@ public Collection createComponents(Client client, ClusterService cluster DataFrameAnalyticsAuditor dataFrameAnalyticsAuditor = new DataFrameAnalyticsAuditor(client, clusterService.getNodeName()); InferenceAuditor inferenceAuditor = new InferenceAuditor(client, clusterService.getNodeName()); this.dataFrameAnalyticsAuditor.set(dataFrameAnalyticsAuditor); + ResultsPersisterService resultsPersisterService = new ResultsPersisterService(client, clusterService, settings); JobResultsProvider jobResultsProvider = new JobResultsProvider(client, settings); - JobResultsPersister jobResultsPersister = new JobResultsPersister(client); - JobDataCountsPersister jobDataCountsPersister = new JobDataCountsPersister(client); + JobResultsPersister jobResultsPersister = new JobResultsPersister(client, resultsPersisterService, anomalyDetectionAuditor); + JobDataCountsPersister jobDataCountsPersister = new JobDataCountsPersister(client, + resultsPersisterService, + anomalyDetectionAuditor); JobConfigProvider jobConfigProvider = new JobConfigProvider(client, xContentRegistry); DatafeedConfigProvider datafeedConfigProvider = new DatafeedConfigProvider(client, xContentRegistry); UpdateJobProcessNotifier notifier = new UpdateJobProcessNotifier(client, clusterService, threadPool); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportRevertModelSnapshotAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportRevertModelSnapshotAction.java index 7b394b580044f..0406f7b9e3370 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportRevertModelSnapshotAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportRevertModelSnapshotAction.java @@ -171,7 +171,7 @@ private ActionListener wrapRevertDataCountsL return ActionListener.wrap(response -> { jobResultsProvider.dataCounts(jobId, counts -> { counts.setLatestRecordTimeStamp(modelSnapshot.getLatestRecordTimeStamp()); - jobDataCountsPersister.persistDataCounts(jobId, counts, new ActionListener() { + jobDataCountsPersister.persistDataCountsAsync(jobId, counts, new ActionListener() { @Override public void onResponse(Boolean aBoolean) { listener.onResponse(response); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataCountsPersister.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataCountsPersister.java index 9866e3f56af19..4925d2c2ac035 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataCountsPersister.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataCountsPersister.java @@ -8,16 +8,19 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.Client; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.DataCounts; +import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; +import org.elasticsearch.xpack.ml.notifications.AnomalyDetectionAuditor; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import java.io.IOException; @@ -33,30 +36,60 @@ public class JobDataCountsPersister { private static final Logger logger = LogManager.getLogger(JobDataCountsPersister.class); + private final ResultsPersisterService resultsPersisterService; private final Client client; + private final AnomalyDetectionAuditor auditor; - public JobDataCountsPersister(Client client) { + public JobDataCountsPersister(Client client, ResultsPersisterService resultsPersisterService, AnomalyDetectionAuditor auditor) { + this.resultsPersisterService = resultsPersisterService; this.client = client; + this.auditor = auditor; } - private XContentBuilder serialiseCounts(DataCounts counts) throws IOException { + private static XContentBuilder serialiseCounts(DataCounts counts) throws IOException { XContentBuilder builder = jsonBuilder(); return counts.toXContent(builder, ToXContent.EMPTY_PARAMS); } /** * Update the job's data counts stats and figures. + * NOTE: This call is synchronous and pauses the calling thread. + * @param jobId Job to update + * @param counts The counts + */ + public void persistDataCounts(String jobId, DataCounts counts) { + try { + resultsPersisterService.indexWithRetry(jobId, + AnomalyDetectorsIndex.resultsWriteAlias(jobId), + counts, + ToXContent.EMPTY_PARAMS, + WriteRequest.RefreshPolicy.NONE, + DataCounts.documentId(jobId), + () -> true, + (msg) -> auditor.warning(jobId, "Job data_counts " + msg)); + } catch (IOException ioe) { + logger.error(() -> new ParameterizedMessage("[{}] Failed writing data_counts stats", jobId), ioe); + } catch (Exception ex) { + logger.error(() -> new ParameterizedMessage("[{}] Failed persisting data_counts stats", jobId), ex); + } + } + + /** + * The same as {@link JobDataCountsPersister#persistDataCounts(String, DataCounts)} but done Asynchronously. * + * Two differences are: + * - The listener is notified on persistence failure + * - If the persistence fails, it is not automatically retried * @param jobId Job to update * @param counts The counts * @param listener ActionType response listener */ - public void persistDataCounts(String jobId, DataCounts counts, ActionListener listener) { + public void persistDataCountsAsync(String jobId, DataCounts counts, ActionListener listener) { try (XContentBuilder content = serialiseCounts(counts)) { final IndexRequest request = new IndexRequest(AnomalyDetectorsIndex.resultsWriteAlias(jobId)) - .id(DataCounts.documentId(jobId)) - .source(content); - executeAsyncWithOrigin(client, ML_ORIGIN, IndexAction.INSTANCE, request, new ActionListener() { + .id(DataCounts.documentId(jobId)) + .source(content); + executeAsyncWithOrigin(client, ML_ORIGIN, IndexAction.INSTANCE, request, new ActionListener<>() { @Override public void onResponse(IndexResponse indexResponse) { listener.onResponse(true); @@ -68,7 +101,9 @@ public void onFailure(Exception e) { } }); } catch (IOException ioe) { - logger.warn((Supplier)() -> new ParameterizedMessage("[{}] Error serialising DataCounts stats", jobId), ioe); + String msg = new ParameterizedMessage("[{}] Failed writing data_counts stats", jobId).getFormattedMessage(); + logger.error(msg, ioe); + listener.onFailure(ExceptionsHelper.serverError(msg, ioe)); } } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersister.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersister.java index 783706259a17b..b9a3fbaa570bb 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersister.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersister.java @@ -8,16 +8,16 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.action.ActionFuture; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.DocWriteRequest; import org.elasticsearch.action.DocWriteResponse.Result; import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; +import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.support.IndicesOptions; -import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.Client; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -39,11 +39,14 @@ import org.elasticsearch.xpack.core.ml.job.results.Influencer; import org.elasticsearch.xpack.core.ml.job.results.ModelPlot; import org.elasticsearch.xpack.core.ml.utils.ToXContentParams; +import org.elasticsearch.xpack.ml.notifications.AnomalyDetectionAuditor; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.function.Supplier; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; @@ -70,24 +73,34 @@ public class JobResultsPersister { private static final Logger logger = LogManager.getLogger(JobResultsPersister.class); private final Client client; + private final ResultsPersisterService resultsPersisterService; + private final AnomalyDetectionAuditor auditor; - public JobResultsPersister(Client client) { + public JobResultsPersister(Client client, + ResultsPersisterService resultsPersisterService, + AnomalyDetectionAuditor auditor) { this.client = client; + this.resultsPersisterService = resultsPersisterService; + this.auditor = auditor; } - public Builder bulkPersisterBuilder(String jobId) { - return new Builder(jobId); + public Builder bulkPersisterBuilder(String jobId, Supplier shouldRetry) { + return new Builder(jobId, resultsPersisterService, shouldRetry); } public class Builder { private BulkRequest bulkRequest; private final String jobId; private final String indexName; + private final Supplier shouldRetry; + private final ResultsPersisterService resultsPersisterService; - private Builder(String jobId) { + private Builder(String jobId, ResultsPersisterService resultsPersisterService, Supplier shouldRetry) { this.jobId = Objects.requireNonNull(jobId); indexName = AnomalyDetectorsIndex.resultsWriteAlias(jobId); bulkRequest = new BulkRequest(); + this.shouldRetry = shouldRetry; + this.resultsPersisterService = resultsPersisterService; } /** @@ -213,14 +226,9 @@ public void executeRequest() { return; } logger.trace("[{}] ES API CALL: bulk request with {} actions", jobId, bulkRequest.numberOfActions()); - - try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(ML_ORIGIN)) { - BulkResponse addRecordsResponse = client.bulk(bulkRequest).actionGet(); - if (addRecordsResponse.hasFailures()) { - logger.error("[{}] Bulk index of results has errors: {}", jobId, addRecordsResponse.buildFailureMessage()); - } - } - + resultsPersisterService.bulkIndexWithRetry(bulkRequest, jobId, shouldRetry, (msg) -> { + auditor.warning(jobId, "Bulk indexing of results failed " + msg); + }); bulkRequest = new BulkRequest(); } @@ -235,10 +243,10 @@ BulkRequest getBulkRequest() { * * @param category The category to be persisted */ - public void persistCategoryDefinition(CategoryDefinition category) { + public void persistCategoryDefinition(CategoryDefinition category, Supplier shouldRetry) { Persistable persistable = new Persistable(category.getJobId(), category, category.getId()); - persistable.persist(AnomalyDetectorsIndex.resultsWriteAlias(category.getJobId())).actionGet(); + persistable.persist(AnomalyDetectorsIndex.resultsWriteAlias(category.getJobId()), shouldRetry); // Don't commit as we expect masses of these updates and they're not // read again by this process } @@ -246,9 +254,9 @@ public void persistCategoryDefinition(CategoryDefinition category) { /** * Persist the quantiles (blocking) */ - public void persistQuantiles(Quantiles quantiles) { + public void persistQuantiles(Quantiles quantiles, Supplier shouldRetry) { Persistable persistable = new Persistable(quantiles.getJobId(), quantiles, Quantiles.documentId(quantiles.getJobId())); - persistable.persist(AnomalyDetectorsIndex.jobStateIndexWriteAlias()).actionGet(); + persistable.persist(AnomalyDetectorsIndex.jobStateIndexWriteAlias(), shouldRetry); } /** @@ -263,20 +271,22 @@ public void persistQuantiles(Quantiles quantiles, WriteRequest.RefreshPolicy ref /** * Persist a model snapshot description */ - public IndexResponse persistModelSnapshot(ModelSnapshot modelSnapshot, WriteRequest.RefreshPolicy refreshPolicy) { + public BulkResponse persistModelSnapshot(ModelSnapshot modelSnapshot, + WriteRequest.RefreshPolicy refreshPolicy, + Supplier shouldRetry) { Persistable persistable = new Persistable(modelSnapshot.getJobId(), modelSnapshot, ModelSnapshot.documentId(modelSnapshot)); persistable.setRefreshPolicy(refreshPolicy); - return persistable.persist(AnomalyDetectorsIndex.resultsWriteAlias(modelSnapshot.getJobId())).actionGet(); + return persistable.persist(AnomalyDetectorsIndex.resultsWriteAlias(modelSnapshot.getJobId()), shouldRetry); } /** * Persist the memory usage data (blocking) */ - public void persistModelSizeStats(ModelSizeStats modelSizeStats) { + public void persistModelSizeStats(ModelSizeStats modelSizeStats, Supplier shouldRetry) { String jobId = modelSizeStats.getJobId(); logger.trace("[{}] Persisting model size stats, for size {}", jobId, modelSizeStats.getModelBytes()); Persistable persistable = new Persistable(jobId, modelSizeStats, modelSizeStats.getId()); - persistable.persist(AnomalyDetectorsIndex.resultsWriteAlias(jobId)).actionGet(); + persistable.persist(AnomalyDetectorsIndex.resultsWriteAlias(jobId), shouldRetry); } /** @@ -341,7 +351,7 @@ public void commitStateWrites(String jobId) { * @param timingStats datafeed timing stats to persist * @param refreshPolicy refresh policy to apply */ - public IndexResponse persistDatafeedTimingStats(DatafeedTimingStats timingStats, WriteRequest.RefreshPolicy refreshPolicy) { + public BulkResponse persistDatafeedTimingStats(DatafeedTimingStats timingStats, WriteRequest.RefreshPolicy refreshPolicy) { String jobId = timingStats.getJobId(); logger.trace("[{}] Persisting datafeed timing stats", jobId); Persistable persistable = new Persistable( @@ -350,7 +360,7 @@ public IndexResponse persistDatafeedTimingStats(DatafeedTimingStats timingStats, new ToXContent.MapParams(Collections.singletonMap(ToXContentParams.FOR_INTERNAL_STORAGE, "true")), DatafeedTimingStats.documentId(timingStats.getJobId())); persistable.setRefreshPolicy(refreshPolicy); - return persistable.persist(AnomalyDetectorsIndex.resultsWriteAlias(jobId)).actionGet(); + return persistable.persist(AnomalyDetectorsIndex.resultsWriteAlias(jobId), () -> true); } private static XContentBuilder toXContentBuilder(ToXContent obj, ToXContent.Params params) throws IOException { @@ -383,10 +393,25 @@ void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { this.refreshPolicy = refreshPolicy; } - ActionFuture persist(String indexName) { - PlainActionFuture actionFuture = PlainActionFuture.newFuture(); - persist(indexName, actionFuture); - return actionFuture; + BulkResponse persist(String indexName, Supplier shouldRetry) { + logCall(indexName); + try { + return resultsPersisterService.indexWithRetry(jobId, + indexName, + object, + params, + refreshPolicy, + id, + shouldRetry, + (msg) -> auditor.warning(jobId, id + " " + msg)); + } catch (IOException e) { + logger.error(new ParameterizedMessage("[{}] Error writing [{}]", jobId, (id == null) ? "auto-generated ID" : id), e); + IndexResponse.Builder notCreatedResponse = new IndexResponse.Builder(); + notCreatedResponse.setResult(Result.NOOP); + return new BulkResponse( + new BulkItemResponse[]{new BulkItemResponse(0, DocWriteRequest.OpType.INDEX, notCreatedResponse.build())}, + 0); + } } void persist(String indexName, ActionListener listener) { @@ -411,4 +436,5 @@ private void logCall(String indexName) { } } } + } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/DataCountsReporter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/DataCountsReporter.java index dff118011b147..1d3a42b5fa9b6 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/DataCountsReporter.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/DataCountsReporter.java @@ -7,7 +7,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.action.ActionListener; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.DataCounts; import org.elasticsearch.xpack.ml.job.persistence.JobDataCountsPersister; @@ -229,13 +228,13 @@ public long getAnalysedFieldsPerRecord() { /** * Report the counts now regardless of whether or not we are at a reporting boundary. */ - public void finishReporting(ActionListener listener) { + public void finishReporting() { Date now = new Date(); incrementalRecordStats.setLastDataTimeStamp(now); totalRecordStats.setLastDataTimeStamp(now); diagnostics.flush(); retrieveDiagnosticsIntermediateResults(); - dataCountsPersister.persistDataCounts(job.getId(), runningTotalStats(), listener); + dataCountsPersister.persistDataCounts(job.getId(), runningTotalStats()); } /** diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessor.java index c9441e9f60c39..422f13926d441 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessor.java @@ -10,6 +10,7 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.DocWriteResponse; +import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.Client; @@ -109,8 +110,7 @@ public AutodetectResultProcessor(Client client, // Visible for testing AutodetectResultProcessor(Client client, AnomalyDetectionAuditor auditor, String jobId, Renormalizer renormalizer, JobResultsPersister persister, AutodetectProcess autodetectProcess, ModelSizeStats latestModelSizeStats, - TimingStats timingStats, - FlushListener flushListener) { + TimingStats timingStats, FlushListener flushListener) { this.client = Objects.requireNonNull(client); this.auditor = Objects.requireNonNull(auditor); this.jobId = Objects.requireNonNull(jobId); @@ -119,7 +119,7 @@ public AutodetectResultProcessor(Client client, this.process = Objects.requireNonNull(autodetectProcess); this.flushListener = Objects.requireNonNull(flushListener); this.latestModelSizeStats = Objects.requireNonNull(latestModelSizeStats); - this.bulkResultsPersister = persister.bulkPersisterBuilder(jobId); + this.bulkResultsPersister = persister.bulkPersisterBuilder(jobId, this::isAlive); this.timingStatsReporter = new TimingStatsReporter(timingStats, bulkResultsPersister); this.deleteInterimRequired = true; } @@ -177,10 +177,7 @@ private void readResults() { LOGGER.trace("[{}] Bucket number {} parsed from output", jobId, bucketCount); } } catch (Exception e) { - if (processKilled) { - throw e; - } - if (process.isProcessAliveAfterWaiting() == false) { + if (isAlive() == false) { throw e; } LOGGER.warn(new ParameterizedMessage("[{}] Error processing autodetect result", jobId), e); @@ -215,7 +212,6 @@ void processResult(AutodetectResult result) { // results are also interim timingStatsReporter.reportBucket(bucket); bulkResultsPersister.persistBucket(bucket).executeRequest(); - ++bucketCount; } List records = result.getRecords(); @@ -228,7 +224,7 @@ void processResult(AutodetectResult result) { } CategoryDefinition categoryDefinition = result.getCategoryDefinition(); if (categoryDefinition != null) { - persister.persistCategoryDefinition(categoryDefinition); + persister.persistCategoryDefinition(categoryDefinition, this::isAlive); } ModelPlot modelPlot = result.getModelPlot(); if (modelPlot != null) { @@ -264,7 +260,9 @@ void processResult(AutodetectResult result) { ModelSnapshot modelSnapshot = result.getModelSnapshot(); if (modelSnapshot != null) { // We need to refresh in order for the snapshot to be available when we try to update the job with it - IndexResponse indexResponse = persister.persistModelSnapshot(modelSnapshot, WriteRequest.RefreshPolicy.IMMEDIATE); + BulkResponse bulkResponse = persister.persistModelSnapshot(modelSnapshot, WriteRequest.RefreshPolicy.IMMEDIATE, this::isAlive); + assert bulkResponse.getItems().length == 1; + IndexResponse indexResponse = bulkResponse.getItems()[0].getResponse(); if (indexResponse.getResult() == DocWriteResponse.Result.CREATED) { updateModelSnapshotOnJob(modelSnapshot); } @@ -272,7 +270,7 @@ void processResult(AutodetectResult result) { Quantiles quantiles = result.getQuantiles(); if (quantiles != null) { LOGGER.debug("[{}] Parsed Quantiles with timestamp {}", jobId, quantiles.getTimestamp()); - persister.persistQuantiles(quantiles); + persister.persistQuantiles(quantiles, this::isAlive); bulkResultsPersister.executeRequest(); if (processKilled == false && renormalizer.isEnabled()) { @@ -316,7 +314,7 @@ private void processModelSizeStats(ModelSizeStats modelSizeStats) { modelSizeStats.getTotalOverFieldCount(), modelSizeStats.getTotalPartitionFieldCount(), modelSizeStats.getBucketAllocationFailuresCount(), modelSizeStats.getMemoryStatus()); - persister.persistModelSizeStats(modelSizeStats); + persister.persistModelSizeStats(modelSizeStats, this::isAlive); notifyModelMemoryStatusChange(modelSizeStats); latestModelSizeStats = modelSizeStats; } @@ -435,6 +433,13 @@ boolean isDeleteInterimRequired() { return deleteInterimRequired; } + private boolean isAlive() { + if (processKilled) { + return false; + } + return process.isProcessAliveAfterWaiting(); + } + void setDeleteInterimRequired(boolean deleteInterimRequired) { this.deleteInterimRequired = deleteInterimRequired; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/CsvDataToProcessWriter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/CsvDataToProcessWriter.java index 887b43d0b5927..efa5f767720c4 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/CsvDataToProcessWriter.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/CsvDataToProcessWriter.java @@ -7,7 +7,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.xpack.ml.job.categorization.CategorizationAnalyzer; import org.elasticsearch.xpack.core.ml.job.config.AnalysisConfig; @@ -128,11 +127,8 @@ public void write(InputStream inputStream, CategorizationAnalyzer categorization transformTimeAndWrite(record, inputFieldCount); } - // This function can throw - dataCountsReporter.finishReporting(ActionListener.wrap( - response -> handler.accept(dataCountsReporter.incrementalStats(), null), - e -> handler.accept(null, e) - )); + dataCountsReporter.finishReporting(); + handler.accept(dataCountsReporter.incrementalStats(), null); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/JsonDataToProcessWriter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/JsonDataToProcessWriter.java index 92fe2c3b0b50a..59c138cba3758 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/JsonDataToProcessWriter.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/JsonDataToProcessWriter.java @@ -7,7 +7,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentFactory; @@ -53,7 +52,7 @@ class JsonDataToProcessWriter extends AbstractDataToProcessWriter { * the OutputStream. No transformation is applied to the data the timestamp * is expected in seconds from the epoch. If any of the fields in * analysisFields or the DataDescriptions - * timeField is missing from the JOSN inputIndex an exception is thrown + * timeField is missing from the JSON inputIndex an exception is thrown */ @Override public void write(InputStream inputStream, CategorizationAnalyzer categorizationAnalyzer, XContentType xContentType, @@ -70,12 +69,8 @@ public void write(InputStream inputStream, CategorizationAnalyzer categorization + "] is not supported by JsonDataToProcessWriter"); } - // this line can throw and will be propagated - dataCountsReporter.finishReporting( - ActionListener.wrap( - response -> handler.accept(dataCountsReporter.incrementalStats(), null), - e -> handler.accept(null, e) - )); + dataCountsReporter.finishReporting(); + handler.accept(dataCountsReporter.incrementalStats(), null); } private void writeJsonXContent(CategorizationAnalyzer categorizationAnalyzer, InputStream inputStream) throws IOException { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/ResultsPersisterService.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/ResultsPersisterService.java new file mode 100644 index 0000000000000..6aad7d6a4f9b8 --- /dev/null +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/ResultsPersisterService.java @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.ml.utils.persistence; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Randomness; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; + +import java.io.IOException; +import java.time.Duration; +import java.util.Arrays; +import java.util.Random; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + + +import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; + +public class ResultsPersisterService { + private static final Logger LOGGER = LogManager.getLogger(ResultsPersisterService.class); + + public static final Setting PERSIST_RESULTS_MAX_RETRIES = Setting.intSetting( + "xpack.ml.persist_results_max_retries", + 20, + 0, + 50, + Setting.Property.Dynamic, + Setting.Property.NodeScope); + private static final int MAX_RETRY_SLEEP_MILLIS = (int)Duration.ofMinutes(15).toMillis(); + private static final int MIN_RETRY_SLEEP_MILLIS = 50; + // Having an exponent higher than this causes integer overflow + private static final int MAX_RETRY_EXPONENT = 24; + + private final Random random = Randomness.get(); + private final Client client; + private volatile int maxFailureRetries; + + public ResultsPersisterService(Client client, ClusterService clusterService, Settings settings) { + this.client = client; + this.maxFailureRetries = PERSIST_RESULTS_MAX_RETRIES.get(settings); + clusterService.getClusterSettings() + .addSettingsUpdateConsumer(PERSIST_RESULTS_MAX_RETRIES, this::setMaxFailureRetries); + } + + void setMaxFailureRetries(int value) { + this.maxFailureRetries = value; + } + + public BulkResponse indexWithRetry(String jobId, + String indexName, + ToXContent object, + ToXContent.Params params, + WriteRequest.RefreshPolicy refreshPolicy, + String id, + Supplier shouldRetry, + Consumer msgHandler) throws IOException { + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(refreshPolicy); + try (XContentBuilder content = object.toXContent(XContentFactory.jsonBuilder(), params)) { + bulkRequest.add(new IndexRequest(indexName).id(id).source(content)); + } + return bulkIndexWithRetry(bulkRequest, jobId, shouldRetry, msgHandler); + } + + public BulkResponse bulkIndexWithRetry(BulkRequest bulkRequest, + String jobId, + Supplier shouldRetry, + Consumer msgHandler) { + int currentMin = MIN_RETRY_SLEEP_MILLIS; + int currentMax = MIN_RETRY_SLEEP_MILLIS; + int currentAttempt = 0; + BulkResponse bulkResponse = null; + while(currentAttempt <= maxFailureRetries) { + bulkResponse = bulkIndex(bulkRequest); + if (bulkResponse.hasFailures() == false) { + return bulkResponse; + } + if (shouldRetry.get() == false) { + throw new ElasticsearchException("[{}] failed to index all results. {}", jobId, bulkResponse.buildFailureMessage()); + } + if (currentAttempt > maxFailureRetries) { + LOGGER.warn("[{}] failed to index after [{}] attempts. Setting [xpack.ml.persist_results_max_retries] was reduced", + jobId, + currentAttempt); + throw new ElasticsearchException("[{}] failed to index all results after [{}] attempts. {}", + jobId, + currentAttempt, + bulkResponse.buildFailureMessage()); + } + currentAttempt++; + // Since we exponentially increase, we don't want force randomness to have an excessively long sleep + if (currentMax < MAX_RETRY_SLEEP_MILLIS) { + currentMin = currentMax; + } + // Exponential backoff calculation taken from: https://en.wikipedia.org/wiki/Exponential_backoff + int uncappedBackoff = ((1 << Math.min(currentAttempt, MAX_RETRY_EXPONENT)) - 1) * (50); + currentMax = Math.min(uncappedBackoff, MAX_RETRY_SLEEP_MILLIS); + // Its good to have a random window along the exponentially increasing curve + // so that not all bulk requests rest for the same amount of time + int randBound = 1 + (currentMax - currentMin); + int randSleep = currentMin + random.nextInt(randBound); + { + String msg = new ParameterizedMessage( + "failed to index after [{}] attempts. Will attempt again in [{}].", + currentAttempt, + TimeValue.timeValueMillis(randSleep).getStringRep()) + .getFormattedMessage(); + LOGGER.warn(()-> new ParameterizedMessage("[{}] {}", jobId, msg)); + msgHandler.accept(msg); + } + // We should only retry the docs that failed. + bulkRequest = buildNewRequestFromFailures(bulkRequest, bulkResponse); + try { + Thread.sleep(randSleep); + } catch (InterruptedException interruptedException) { + LOGGER.warn( + new ParameterizedMessage("[{}] failed to index after [{}] attempts due to interruption", + jobId, + currentAttempt), + interruptedException); + Thread.currentThread().interrupt(); + } + } + String bulkFailureMessage = bulkResponse == null ? "" : bulkResponse.buildFailureMessage(); + LOGGER.warn("[{}] failed to index after [{}] attempts.", jobId, currentAttempt); + throw new ElasticsearchException("[{}] failed to index all results after [{}] attempts. {}", + jobId, + currentAttempt, + bulkFailureMessage); + } + + private BulkResponse bulkIndex(BulkRequest bulkRequest) { + try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(ML_ORIGIN)) { + return client.bulk(bulkRequest).actionGet(); + } + } + + private BulkRequest buildNewRequestFromFailures(BulkRequest bulkRequest, BulkResponse bulkResponse) { + // If we failed, lets set the bulkRequest to be a collection of the failed requests + BulkRequest bulkRequestOfFailures = new BulkRequest(); + Set failedDocIds = Arrays.stream(bulkResponse.getItems()) + .filter(BulkItemResponse::isFailed) + .map(BulkItemResponse::getId) + .collect(Collectors.toSet()); + bulkRequest.requests().forEach(docWriteRequest -> { + if (failedDocIds.contains(docWriteRequest.id())) { + bulkRequestOfFailures.add(docWriteRequest); + } + }); + return bulkRequestOfFailures; + } + +} diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/AutodetectResultProcessorIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/AutodetectResultProcessorIT.java index f6ec2fc9b89f1..a46e83d2488fe 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/AutodetectResultProcessorIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/AutodetectResultProcessorIT.java @@ -6,13 +6,19 @@ package org.elasticsearch.xpack.ml.integration; import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.cluster.routing.OperationRouting; import org.elasticsearch.cluster.routing.UnassignedInfo; +import org.elasticsearch.cluster.service.ClusterApplierService; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.cluster.service.MasterService; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.index.reindex.ReindexPlugin; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ml.action.DeleteJobAction; import org.elasticsearch.xpack.core.ml.action.PutJobAction; import org.elasticsearch.xpack.core.action.util.QueryPage; @@ -33,6 +39,7 @@ import org.elasticsearch.xpack.core.ml.job.results.ModelPlot; import org.elasticsearch.xpack.ml.LocalStateMachineLearning; import org.elasticsearch.xpack.ml.MlSingleNodeTestCase; +import org.elasticsearch.xpack.ml.inference.ingest.InferenceProcessor; import org.elasticsearch.xpack.ml.job.persistence.BucketsQueryBuilder; import org.elasticsearch.xpack.ml.job.persistence.InfluencersQueryBuilder; import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; @@ -46,10 +53,12 @@ import org.elasticsearch.xpack.ml.job.results.CategoryDefinitionTests; import org.elasticsearch.xpack.ml.job.results.ModelPlotTests; import org.elasticsearch.xpack.ml.notifications.AnomalyDetectionAuditor; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import org.junit.After; import org.junit.Before; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -77,6 +86,7 @@ public class AutodetectResultProcessorIT extends MlSingleNodeTestCase { private AutodetectResultProcessor resultProcessor; private Renormalizer renormalizer; private AutodetectProcess process; + private ResultsPersisterService resultsPersisterService; @Override protected Collection> getPlugins() { @@ -92,12 +102,24 @@ public void createComponents() throws Exception { renormalizer = mock(Renormalizer.class); process = mock(AutodetectProcess.class); capturedUpdateModelSnapshotOnJobRequests = new ArrayList<>(); + ThreadPool tp = mock(ThreadPool.class); + Settings settings = Settings.builder().put("node.name", "InferenceProcessorFactoryTests_node").build(); + ClusterSettings clusterSettings = new ClusterSettings(settings, + new HashSet<>(Arrays.asList(InferenceProcessor.MAX_INFERENCE_PROCESSORS, + MasterService.MASTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING, + OperationRouting.USE_ADAPTIVE_REPLICA_SELECTION_SETTING, + ClusterService.USER_DEFINED_META_DATA, + ResultsPersisterService.PERSIST_RESULTS_MAX_RETRIES, + ClusterApplierService.CLUSTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING))); + ClusterService clusterService = new ClusterService(settings, clusterSettings, tp); + + resultsPersisterService = new ResultsPersisterService(client(), clusterService, settings); resultProcessor = new AutodetectResultProcessor( client(), auditor, JOB_ID, renormalizer, - new JobResultsPersister(client()), + new JobResultsPersister(client(), resultsPersisterService, new AnomalyDetectionAuditor(client(), "test_node")), process, new ModelSizeStats.Builder(JOB_ID).build(), new TimingStats(JOB_ID)) { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/EstablishedMemUsageIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/EstablishedMemUsageIT.java index a026d5d6c337b..1a7f60cd81d54 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/EstablishedMemUsageIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/EstablishedMemUsageIT.java @@ -5,22 +5,34 @@ */ package org.elasticsearch.xpack.ml.integration; +import org.elasticsearch.cluster.routing.OperationRouting; +import org.elasticsearch.cluster.service.ClusterApplierService; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.cluster.service.MasterService; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ml.action.PutJobAction; import org.elasticsearch.xpack.core.ml.job.config.AnalysisConfig; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.ModelSizeStats; import org.elasticsearch.xpack.core.ml.job.results.Bucket; +import org.elasticsearch.xpack.ml.inference.ingest.InferenceProcessor; import org.elasticsearch.xpack.ml.job.persistence.JobResultsProvider; import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; +import org.elasticsearch.xpack.ml.notifications.AnomalyDetectionAuditor; import org.elasticsearch.xpack.ml.support.BaseMlIntegTestCase; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import org.junit.Before; +import java.util.Arrays; import java.util.Date; +import java.util.HashSet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import static org.hamcrest.CoreMatchers.equalTo; +import static org.mockito.Mockito.mock; public class EstablishedMemUsageIT extends BaseMlIntegTestCase { @@ -32,8 +44,21 @@ public class EstablishedMemUsageIT extends BaseMlIntegTestCase { @Before public void createComponents() { Settings settings = nodeSettings(0); + ThreadPool tp = mock(ThreadPool.class); + ClusterSettings clusterSettings = new ClusterSettings(settings, + new HashSet<>(Arrays.asList(InferenceProcessor.MAX_INFERENCE_PROCESSORS, + MasterService.MASTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING, + ResultsPersisterService.PERSIST_RESULTS_MAX_RETRIES, + OperationRouting.USE_ADAPTIVE_REPLICA_SELECTION_SETTING, + ClusterService.USER_DEFINED_META_DATA, + ClusterApplierService.CLUSTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING))); + ClusterService clusterService = new ClusterService(settings, clusterSettings, tp); + + ResultsPersisterService resultsPersisterService = new ResultsPersisterService(client(), clusterService, settings); jobResultsProvider = new JobResultsProvider(client(), settings); - jobResultsPersister = new JobResultsPersister(client()); + jobResultsPersister = new JobResultsPersister(client(), + resultsPersisterService, + new AnomalyDetectionAuditor(client(), "test_node")); } public void testEstablishedMem_givenNoResults() throws Exception { @@ -222,7 +247,7 @@ private void initClusterAndJob(String jobId) { } private void createBuckets(String jobId, int count) { - JobResultsPersister.Builder builder = jobResultsPersister.bulkPersisterBuilder(jobId); + JobResultsPersister.Builder builder = jobResultsPersister.bulkPersisterBuilder(jobId, () -> true); for (int i = 1; i <= count; ++i) { Bucket bucket = new Bucket(jobId, new Date(bucketSpan * i), bucketSpan); builder.persistBucket(bucket); @@ -235,7 +260,7 @@ private ModelSizeStats createModelSizeStats(String jobId, int bucketNum, long mo .setTimestamp(new Date(bucketSpan * bucketNum)) .setLogTime(new Date(bucketSpan * bucketNum + randomIntBetween(1, 1000))) .setModelBytes(modelBytes).build(); - jobResultsPersister.persistModelSizeStats(modelSizeStats); + jobResultsPersister.persistModelSizeStats(modelSizeStats, () -> true); return modelSizeStats; } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java index bde16f7e9ac07..540ee7b5adfc3 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java @@ -15,14 +15,20 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.cluster.metadata.MappingMetaData; +import org.elasticsearch.cluster.routing.OperationRouting; import org.elasticsearch.cluster.routing.UnassignedInfo; +import org.elasticsearch.cluster.service.ClusterApplierService; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.cluster.service.MasterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.settings.ClusterSettings; 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.XContentFactory; +import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ml.MlMetaIndex; import org.elasticsearch.xpack.core.ml.MlMetadata; import org.elasticsearch.xpack.core.ml.action.PutJobAction; @@ -46,12 +52,15 @@ import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.Quantiles; import org.elasticsearch.xpack.core.ml.utils.ToXContentParams; import org.elasticsearch.xpack.ml.MlSingleNodeTestCase; +import org.elasticsearch.xpack.ml.inference.ingest.InferenceProcessor; import org.elasticsearch.xpack.ml.job.persistence.CalendarQueryBuilder; import org.elasticsearch.xpack.ml.job.persistence.JobDataCountsPersister; import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; import org.elasticsearch.xpack.ml.job.persistence.JobResultsProvider; import org.elasticsearch.xpack.ml.job.persistence.ScheduledEventsQueryBuilder; import org.elasticsearch.xpack.ml.job.process.autodetect.params.AutodetectParams; +import org.elasticsearch.xpack.ml.notifications.AnomalyDetectionAuditor; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import org.junit.Before; import java.io.IOException; @@ -73,17 +82,32 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.collection.IsEmptyCollection.empty; import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.mock; public class JobResultsProviderIT extends MlSingleNodeTestCase { private JobResultsProvider jobProvider; + private ResultsPersisterService resultsPersisterService; + private AnomalyDetectionAuditor auditor; @Before public void createComponents() throws Exception { Settings.Builder builder = Settings.builder() .put(UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), TimeValue.timeValueSeconds(1)); jobProvider = new JobResultsProvider(client(), builder.build()); + ThreadPool tp = mock(ThreadPool.class); + ClusterSettings clusterSettings = new ClusterSettings(builder.build(), + new HashSet<>(Arrays.asList(InferenceProcessor.MAX_INFERENCE_PROCESSORS, + MasterService.MASTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING, + OperationRouting.USE_ADAPTIVE_REPLICA_SELECTION_SETTING, + ResultsPersisterService.PERSIST_RESULTS_MAX_RETRIES, + ClusterService.USER_DEFINED_META_DATA, + ClusterApplierService.CLUSTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING))); + ClusterService clusterService = new ClusterService(builder.build(), clusterSettings, tp); + + resultsPersisterService = new ResultsPersisterService(client(), clusterService, builder.build()); + auditor = new AnomalyDetectionAuditor(client(), "test_node"); waitForMlTemplates(); } @@ -574,29 +598,9 @@ private void indexScheduledEvents(List events) throws IOExceptio } } - private void indexDataCounts(DataCounts counts, String jobId) throws Exception { - JobDataCountsPersister persister = new JobDataCountsPersister(client()); - - AtomicReference errorHolder = new AtomicReference<>(); - CountDownLatch latch = new CountDownLatch(1); - persister.persistDataCounts(jobId, counts, new ActionListener() { - @Override - public void onResponse(Boolean aBoolean) { - assertTrue(aBoolean); - latch.countDown(); - } - - @Override - public void onFailure(Exception e) { - errorHolder.set(e); - latch.countDown(); - } - }); - - latch.await(); - if (errorHolder.get() != null) { - throw errorHolder.get(); - } + private void indexDataCounts(DataCounts counts, String jobId) { + JobDataCountsPersister persister = new JobDataCountsPersister(client(), resultsPersisterService, auditor); + persister.persistDataCounts(jobId, counts); } private void indexFilters(List filters) throws IOException { @@ -616,18 +620,18 @@ private void indexFilters(List filters) throws IOException { } private void indexModelSizeStats(ModelSizeStats modelSizeStats) { - JobResultsPersister persister = new JobResultsPersister(client()); - persister.persistModelSizeStats(modelSizeStats); + JobResultsPersister persister = new JobResultsPersister(client(), resultsPersisterService, auditor); + persister.persistModelSizeStats(modelSizeStats, () -> true); } private void indexModelSnapshot(ModelSnapshot snapshot) { - JobResultsPersister persister = new JobResultsPersister(client()); - persister.persistModelSnapshot(snapshot, WriteRequest.RefreshPolicy.IMMEDIATE); + JobResultsPersister persister = new JobResultsPersister(client(), resultsPersisterService, auditor); + persister.persistModelSnapshot(snapshot, WriteRequest.RefreshPolicy.IMMEDIATE, () -> true); } private void indexQuantiles(Quantiles quantiles) { - JobResultsPersister persister = new JobResultsPersister(client()); - persister.persistQuantiles(quantiles); + JobResultsPersister persister = new JobResultsPersister(client(), resultsPersisterService, auditor); + persister.persistQuantiles(quantiles, () -> true); } private void indexCalendars(List calendars) throws IOException { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersisterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersisterTests.java index 09e5cf9ce6331..7bf719878c157 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersisterTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersisterTests.java @@ -6,17 +6,19 @@ package org.elasticsearch.xpack.ml.job.persistence; import org.elasticsearch.action.ActionFuture; -import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.routing.OperationRouting; +import org.elasticsearch.cluster.service.ClusterApplierService; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.cluster.service.MasterService; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; @@ -27,19 +29,23 @@ import org.elasticsearch.xpack.core.ml.job.results.Influencer; import org.elasticsearch.xpack.core.ml.job.results.ModelPlot; import org.elasticsearch.xpack.core.ml.utils.ExponentialAverageCalculationContext; +import org.elasticsearch.xpack.ml.inference.ingest.InferenceProcessor; +import org.elasticsearch.xpack.ml.notifications.AnomalyDetectionAuditor; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import org.mockito.ArgumentCaptor; -import java.io.IOException; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -51,7 +57,7 @@ public class JobResultsPersisterTests extends ESTestCase { private static final String JOB_ID = "foo"; - public void testPersistBucket_OneRecord() throws IOException { + public void testPersistBucket_OneRecord() { ArgumentCaptor captor = ArgumentCaptor.forClass(BulkRequest.class); Client client = mockClient(captor); Bucket bucket = new Bucket("foo", new Date(), 123456); @@ -72,8 +78,8 @@ public void testPersistBucket_OneRecord() throws IOException { AnomalyRecord record = new AnomalyRecord(JOB_ID, new Date(), 600); bucket.setRecords(Collections.singletonList(record)); - JobResultsPersister persister = new JobResultsPersister(client); - persister.bulkPersisterBuilder(JOB_ID).persistBucket(bucket).executeRequest(); + JobResultsPersister persister = new JobResultsPersister(client, buildResultsPersisterService(client), makeAuditor()); + persister.bulkPersisterBuilder(JOB_ID, () -> true).persistBucket(bucket).executeRequest(); BulkRequest bulkRequest = captor.getValue(); assertEquals(2, bulkRequest.numberOfActions()); @@ -94,7 +100,7 @@ public void testPersistBucket_OneRecord() throws IOException { assertTrue(s.matches(".*raw_anomaly_score.:19\\.19.*")); } - public void testPersistRecords() throws IOException { + public void testPersistRecords() { ArgumentCaptor captor = ArgumentCaptor.forClass(BulkRequest.class); Client client = mockClient(captor); @@ -124,8 +130,8 @@ public void testPersistRecords() throws IOException { typicals.add(998765.3); r1.setTypical(typicals); - JobResultsPersister persister = new JobResultsPersister(client); - persister.bulkPersisterBuilder(JOB_ID).persistRecords(records).executeRequest(); + JobResultsPersister persister = new JobResultsPersister(client, buildResultsPersisterService(client), makeAuditor()); + persister.bulkPersisterBuilder(JOB_ID, () -> true).persistRecords(records).executeRequest(); BulkRequest bulkRequest = captor.getValue(); assertEquals(1, bulkRequest.numberOfActions()); @@ -149,7 +155,7 @@ public void testPersistRecords() throws IOException { assertTrue(s.matches(".*over_field_value.:.overValue.*")); } - public void testPersistInfluencers() throws IOException { + public void testPersistInfluencers() { ArgumentCaptor captor = ArgumentCaptor.forClass(BulkRequest.class); Client client = mockClient(captor); @@ -160,8 +166,8 @@ public void testPersistInfluencers() throws IOException { inf.setProbability(0.4); influencers.add(inf); - JobResultsPersister persister = new JobResultsPersister(client); - persister.bulkPersisterBuilder(JOB_ID).persistInfluencers(influencers).executeRequest(); + JobResultsPersister persister = new JobResultsPersister(client, buildResultsPersisterService(client), makeAuditor()); + persister.bulkPersisterBuilder(JOB_ID, () -> true).persistInfluencers(influencers).executeRequest(); BulkRequest bulkRequest = captor.getValue(); assertEquals(1, bulkRequest.numberOfActions()); @@ -176,7 +182,7 @@ public void testPersistInfluencers() throws IOException { public void testExecuteRequest_ClearsBulkRequest() { ArgumentCaptor captor = ArgumentCaptor.forClass(BulkRequest.class); Client client = mockClient(captor); - JobResultsPersister persister = new JobResultsPersister(client); + JobResultsPersister persister = new JobResultsPersister(client, buildResultsPersisterService(client), makeAuditor()); List influencers = new ArrayList<>(); Influencer inf = new Influencer(JOB_ID, "infName1", "infValue1", new Date(), 600); @@ -185,7 +191,7 @@ public void testExecuteRequest_ClearsBulkRequest() { inf.setProbability(0.4); influencers.add(inf); - JobResultsPersister.Builder builder = persister.bulkPersisterBuilder(JOB_ID); + JobResultsPersister.Builder builder = persister.bulkPersisterBuilder(JOB_ID, () -> true); builder.persistInfluencers(influencers).executeRequest(); assertEquals(0, builder.getBulkRequest().numberOfActions()); } @@ -193,9 +199,9 @@ public void testExecuteRequest_ClearsBulkRequest() { public void testBulkRequestExecutesWhenReachMaxDocs() { ArgumentCaptor captor = ArgumentCaptor.forClass(BulkRequest.class); Client client = mockClient(captor); - JobResultsPersister persister = new JobResultsPersister(client); + JobResultsPersister persister = new JobResultsPersister(client, buildResultsPersisterService(client), makeAuditor()); - JobResultsPersister.Builder bulkBuilder = persister.bulkPersisterBuilder("foo"); + JobResultsPersister.Builder bulkBuilder = persister.bulkPersisterBuilder("foo", () -> true); ModelPlot modelPlot = new ModelPlot("foo", new Date(), 123456, 0); for (int i=0; i<=JobRenormalizedResultsPersister.BULK_LIMIT; i++) { bulkBuilder.persistModelPlot(modelPlot); @@ -210,11 +216,11 @@ public void testPersistTimingStats() { ArgumentCaptor bulkRequestCaptor = ArgumentCaptor.forClass(BulkRequest.class); Client client = mockClient(bulkRequestCaptor); - JobResultsPersister persister = new JobResultsPersister(client); + JobResultsPersister persister = new JobResultsPersister(client, buildResultsPersisterService(client), makeAuditor()); TimingStats timingStats = new TimingStats( "foo", 7, 1.0, 2.0, 1.23, 7.89, new ExponentialAverageCalculationContext(600.0, Instant.ofEpochMilli(123456789), 60.0)); - persister.bulkPersisterBuilder(JOB_ID).persistTimingStats(timingStats).executeRequest(); + persister.bulkPersisterBuilder(JOB_ID, () -> true).persistTimingStats(timingStats).executeRequest(); verify(client, times(1)).bulk(bulkRequestCaptor.capture()); BulkRequest bulkRequest = bulkRequestCaptor.getValue(); @@ -245,28 +251,20 @@ public void testPersistTimingStats() { @SuppressWarnings({"unchecked", "rawtypes"}) public void testPersistDatafeedTimingStats() { Client client = mockClient(ArgumentCaptor.forClass(BulkRequest.class)); - doAnswer( - invocationOnMock -> { - // Take the listener passed to client::index as 2nd argument - ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; - // Handle the response on the listener - listener.onResponse(new IndexResponse(new ShardId("test", "test", 0), "test", 0, 0, 0, false)); - return null; - }) - .when(client).index(any(), any(ActionListener.class)); - - JobResultsPersister persister = new JobResultsPersister(client); + JobResultsPersister persister = new JobResultsPersister(client, buildResultsPersisterService(client), makeAuditor()); DatafeedTimingStats timingStats = new DatafeedTimingStats( "foo", 6, 66, 666.0, new ExponentialAverageCalculationContext(600.0, Instant.ofEpochMilli(123456789), 60.0)); persister.persistDatafeedTimingStats(timingStats, WriteRequest.RefreshPolicy.IMMEDIATE); - ArgumentCaptor indexRequestCaptor = ArgumentCaptor.forClass(IndexRequest.class); - verify(client, times(1)).index(indexRequestCaptor.capture(), any(ActionListener.class)); - IndexRequest indexRequest = indexRequestCaptor.getValue(); + ArgumentCaptor indexRequestCaptor = ArgumentCaptor.forClass(BulkRequest.class); + verify(client, times(1)).bulk(indexRequestCaptor.capture()); + + // Refresh policy is set on the bulk request, not the individual index requests + assertThat(indexRequestCaptor.getValue().getRefreshPolicy(), equalTo(WriteRequest.RefreshPolicy.IMMEDIATE)); + IndexRequest indexRequest = (IndexRequest)indexRequestCaptor.getValue().requests().get(0); assertThat(indexRequest.index(), equalTo(".ml-anomalies-.write-foo")); assertThat(indexRequest.id(), equalTo("foo_datafeed_timing_stats")); - assertThat(indexRequest.getRefreshPolicy(), equalTo(WriteRequest.RefreshPolicy.IMMEDIATE)); assertThat( indexRequest.sourceAsMap(), equalTo( @@ -285,15 +283,51 @@ public void testPersistDatafeedTimingStats() { verifyNoMoreInteractions(client); } - @SuppressWarnings({"unchecked"}) private Client mockClient(ArgumentCaptor captor) { + return mockClientWithResponse(captor, new BulkResponse(new BulkItemResponse[0], 0L)); + } + + @SuppressWarnings("unchecked") + private Client mockClientWithResponse(ArgumentCaptor captor, BulkResponse... responses) { Client client = mock(Client.class); ThreadPool threadPool = mock(ThreadPool.class); when(client.threadPool()).thenReturn(threadPool); when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); - ActionFuture future = mock(ActionFuture.class); - when(future.actionGet()).thenReturn(new BulkResponse(new BulkItemResponse[0], 0L)); - when(client.bulk(captor.capture())).thenReturn(future); + List> futures = new ArrayList<>(responses.length - 1); + ActionFuture future1 = makeFuture(responses[0]); + for (int i = 1; i < responses.length; i++) { + futures.add(makeFuture(responses[i])); + } + when(client.bulk(captor.capture())).thenReturn(future1, futures.toArray(ActionFuture[]::new)); return client; } + + @SuppressWarnings("unchecked") + private static ActionFuture makeFuture(BulkResponse response) { + ActionFuture future = mock(ActionFuture.class); + when(future.actionGet()).thenReturn(response); + return future; + } + + private ResultsPersisterService buildResultsPersisterService(Client client) { + ThreadPool tp = mock(ThreadPool.class); + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, + new HashSet<>(Arrays.asList(InferenceProcessor.MAX_INFERENCE_PROCESSORS, + MasterService.MASTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING, + OperationRouting.USE_ADAPTIVE_REPLICA_SELECTION_SETTING, + ResultsPersisterService.PERSIST_RESULTS_MAX_RETRIES, + ClusterService.USER_DEFINED_META_DATA, + ClusterApplierService.CLUSTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING))); + ClusterService clusterService = new ClusterService(Settings.EMPTY, clusterSettings, tp); + + return new ResultsPersisterService(client, clusterService, Settings.EMPTY); + } + + private AnomalyDetectionAuditor makeAuditor() { + AnomalyDetectionAuditor anomalyDetectionAuditor = mock(AnomalyDetectionAuditor.class); + doNothing().when(anomalyDetectionAuditor).warning(any(), any()); + doNothing().when(anomalyDetectionAuditor).info(any(), any()); + doNothing().when(anomalyDetectionAuditor).error(any(), any()); + return anomalyDetectionAuditor; + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/DataCountsReporterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/DataCountsReporterTests.java index 5415b46019196..e9faff382edb3 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/DataCountsReporterTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/DataCountsReporterTests.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.ml.job.process; -import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.ml.job.config.AnalysisConfig; @@ -17,7 +16,6 @@ import org.junit.Before; import org.mockito.Mockito; -import java.io.IOException; import java.util.Arrays; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -26,6 +24,7 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class DataCountsReporterTests extends ESTestCase { @@ -50,14 +49,14 @@ public void setUpMocks() { jobDataCountsPersister = Mockito.mock(JobDataCountsPersister.class); } - public void testSimpleConstructor() throws Exception { + public void testSimpleConstructor() { DataCountsReporter dataCountsReporter = new DataCountsReporter(job, new DataCounts(job.getId()), jobDataCountsPersister); DataCounts stats = dataCountsReporter.incrementalStats(); assertNotNull(stats); assertAllCountFieldsEqualZero(stats); } - public void testComplexConstructor() throws Exception { + public void testComplexConstructor() { DataCounts counts = new DataCounts("foo", 1L, 1L, 2L, 0L, 3L, 4L, 5L, 6L, 7L, 8L, new Date(), new Date(), new Date(), new Date(), new Date()); @@ -77,7 +76,7 @@ public void testComplexConstructor() throws Exception { assertNull(stats.getEarliestRecordTimeStamp()); } - public void testResetIncrementalCounts() throws Exception { + public void testResetIncrementalCounts() { DataCountsReporter dataCountsReporter = new DataCountsReporter(job, new DataCounts(job.getId()), jobDataCountsPersister); DataCounts stats = dataCountsReporter.incrementalStats(); assertNotNull(stats); @@ -119,7 +118,7 @@ public void testResetIncrementalCounts() throws Exception { assertEquals(602000, dataCountsReporter.runningTotalStats().getLatestRecordTimeStamp().getTime()); // send 'flush' signal - dataCountsReporter.finishReporting(ActionListener.wrap(r -> {}, e -> {})); + dataCountsReporter.finishReporting(); assertEquals(2, dataCountsReporter.runningTotalStats().getBucketCount()); assertEquals(1, dataCountsReporter.runningTotalStats().getEmptyBucketCount()); assertEquals(0, dataCountsReporter.runningTotalStats().getSparseBucketCount()); @@ -129,7 +128,7 @@ public void testResetIncrementalCounts() throws Exception { assertEquals(0, dataCountsReporter.incrementalStats().getSparseBucketCount()); } - public void testReportLatestTimeIncrementalStats() throws IOException { + public void testReportLatestTimeIncrementalStats() { DataCountsReporter dataCountsReporter = new DataCountsReporter(job, new DataCounts(job.getId()), jobDataCountsPersister); dataCountsReporter.startNewIncrementalCount(); dataCountsReporter.reportLatestTimeIncrementalStats(5001L); @@ -157,7 +156,7 @@ public void testReportRecordsWritten() { assertEquals(dataCountsReporter.incrementalStats(), dataCountsReporter.runningTotalStats()); - verify(jobDataCountsPersister, never()).persistDataCounts(anyString(), any(DataCounts.class), any()); + verify(jobDataCountsPersister, never()).persistDataCounts(anyString(), any(DataCounts.class)); } public void testReportRecordsWritten_Given9999Records() { @@ -256,7 +255,7 @@ public void testFinishReporting() { dataCountsReporter.reportRecordWritten(5, 2000); dataCountsReporter.reportRecordWritten(5, 3000); dataCountsReporter.reportMissingField(); - dataCountsReporter.finishReporting(ActionListener.wrap(r -> {}, e -> {})); + dataCountsReporter.finishReporting(); long lastReportedTimeMs = dataCountsReporter.incrementalStats().getLastDataTimeStamp().getTime(); // check last data time is equal to now give or take a second @@ -266,11 +265,11 @@ public void testFinishReporting() { dataCountsReporter.runningTotalStats().getLastDataTimeStamp()); dc.setLastDataTimeStamp(dataCountsReporter.incrementalStats().getLastDataTimeStamp()); - Mockito.verify(jobDataCountsPersister, Mockito.times(1)).persistDataCounts(eq("sr"), eq(dc), any()); + verify(jobDataCountsPersister, times(1)).persistDataCounts(eq("sr"), eq(dc)); assertEquals(dc, dataCountsReporter.incrementalStats()); } - private void assertAllCountFieldsEqualZero(DataCounts stats) throws Exception { + private void assertAllCountFieldsEqualZero(DataCounts stats) { assertEquals(0L, stats.getProcessedRecordCount()); assertEquals(0L, stats.getProcessedFieldCount()); assertEquals(0L, stats.getInputBytes()); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectCommunicatorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectCommunicatorTests.java index 4562779fc292f..e9fbaf4bbb0d5 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectCommunicatorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectCommunicatorTests.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.ml.job.process.autodetect; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentType; @@ -55,6 +54,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -235,10 +235,7 @@ private AutodetectCommunicator createAutodetectCommunicator(ExecutorService exec AutodetectResultProcessor autodetectResultProcessor, BiConsumer finishHandler) throws IOException { DataCountsReporter dataCountsReporter = mock(DataCountsReporter.class); - doAnswer(invocation -> { - ((ActionListener) invocation.getArguments()[0]).onResponse(true); - return null; - }).when(dataCountsReporter).finishReporting(any()); + doNothing().when(dataCountsReporter).finishReporting(); return new AutodetectCommunicator(createJobDetails(), environment, autodetectProcess, stateStreamer, dataCountsReporter, autodetectResultProcessor, finishHandler, new NamedXContentRegistry(Collections.emptyList()), executorService); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectProcessManagerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectProcessManagerTests.java index 72bdf45a96c4a..09a92e57d0432 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectProcessManagerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectProcessManagerTests.java @@ -54,6 +54,7 @@ import org.elasticsearch.xpack.ml.job.process.normalizer.NormalizerFactory; import org.elasticsearch.xpack.ml.notifications.AnomalyDetectionAuditor; import org.elasticsearch.xpack.ml.process.NativeStorageProvider; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import org.junit.Before; import org.mockito.ArgumentCaptor; @@ -61,6 +62,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; @@ -143,7 +145,7 @@ public void setup() throws Exception { jobManager = mock(JobManager.class); jobResultsProvider = mock(JobResultsProvider.class); jobResultsPersister = mock(JobResultsPersister.class); - when(jobResultsPersister.bulkPersisterBuilder(any())).thenReturn(mock(JobResultsPersister.Builder.class)); + when(jobResultsPersister.bulkPersisterBuilder(any(), any())).thenReturn(mock(JobResultsPersister.Builder.class)); jobDataCountsPersister = mock(JobDataCountsPersister.class); autodetectCommunicator = mock(AutodetectCommunicator.class); autodetectFactory = mock(AutodetectProcessFactory.class); @@ -151,7 +153,9 @@ public void setup() throws Exception { auditor = mock(AnomalyDetectionAuditor.class); clusterService = mock(ClusterService.class); ClusterSettings clusterSettings = - new ClusterSettings(Settings.EMPTY, Collections.singleton(MachineLearning.MAX_OPEN_JOBS_PER_NODE)); + new ClusterSettings(Settings.EMPTY, + new HashSet<>(Arrays.asList(MachineLearning.MAX_OPEN_JOBS_PER_NODE, + ResultsPersisterService.PERSIST_RESULTS_MAX_RETRIES))); when(clusterService.getClusterSettings()).thenReturn(clusterSettings); MetaData metaData = mock(MetaData.class); SortedMap aliasOrIndexSortedMap = new TreeMap<>(); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessorTests.java index 63ca73444b540..2f31015b575c7 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessorTests.java @@ -8,6 +8,9 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.Version; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.Client; @@ -95,7 +98,7 @@ public void setUpMocks() { renormalizer = mock(Renormalizer.class); persister = mock(JobResultsPersister.class); bulkBuilder = mock(JobResultsPersister.Builder.class); - when(persister.bulkPersisterBuilder(JOB_ID)).thenReturn(bulkBuilder); + when(persister.bulkPersisterBuilder(eq(JOB_ID), any())).thenReturn(bulkBuilder); process = mock(AutodetectProcess.class); flushListener = mock(FlushListener.class); processorUnderTest = new AutodetectResultProcessor( @@ -125,12 +128,12 @@ public void testProcess() throws TimeoutException { assertThat(processorUnderTest.completionLatch.getCount(), is(equalTo(0L))); verify(renormalizer).waitUntilIdle(); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister).commitResultWrites(JOB_ID); verify(persister).commitStateWrites(JOB_ID); } - public void testProcessResult_bucket() { + public void testProcessResult_bucket() throws Exception { when(bulkBuilder.persistTimingStats(any(TimingStats.class))).thenReturn(bulkBuilder); when(bulkBuilder.persistBucket(any(Bucket.class))).thenReturn(bulkBuilder); AutodetectResult result = mock(AutodetectResult.class); @@ -143,11 +146,11 @@ public void testProcessResult_bucket() { verify(bulkBuilder).persistTimingStats(any(TimingStats.class)); verify(bulkBuilder).persistBucket(bucket); verify(bulkBuilder).executeRequest(); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister, never()).deleteInterimResults(JOB_ID); } - public void testProcessResult_bucket_deleteInterimRequired() { + public void testProcessResult_bucket_deleteInterimRequired() throws Exception { when(bulkBuilder.persistTimingStats(any(TimingStats.class))).thenReturn(bulkBuilder); when(bulkBuilder.persistBucket(any(Bucket.class))).thenReturn(bulkBuilder); AutodetectResult result = mock(AutodetectResult.class); @@ -160,11 +163,11 @@ public void testProcessResult_bucket_deleteInterimRequired() { verify(bulkBuilder).persistTimingStats(any(TimingStats.class)); verify(bulkBuilder).persistBucket(bucket); verify(bulkBuilder).executeRequest(); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister).deleteInterimResults(JOB_ID); } - public void testProcessResult_records() { + public void testProcessResult_records() throws Exception { AutodetectResult result = mock(AutodetectResult.class); List records = Arrays.asList( @@ -177,10 +180,10 @@ public void testProcessResult_records() { verify(bulkBuilder).persistRecords(records); verify(bulkBuilder, never()).executeRequest(); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); } - public void testProcessResult_influencers() { + public void testProcessResult_influencers() throws Exception { AutodetectResult result = mock(AutodetectResult.class); List influencers = Arrays.asList( @@ -193,10 +196,10 @@ public void testProcessResult_influencers() { verify(bulkBuilder).persistInfluencers(influencers); verify(bulkBuilder, never()).executeRequest(); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); } - public void testProcessResult_categoryDefinition() { + public void testProcessResult_categoryDefinition() throws Exception { AutodetectResult result = mock(AutodetectResult.class); CategoryDefinition categoryDefinition = mock(CategoryDefinition.class); when(result.getCategoryDefinition()).thenReturn(categoryDefinition); @@ -205,11 +208,11 @@ public void testProcessResult_categoryDefinition() { processorUnderTest.processResult(result); verify(bulkBuilder, never()).executeRequest(); - verify(persister).persistCategoryDefinition(categoryDefinition); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).persistCategoryDefinition(eq(categoryDefinition), any()); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); } - public void testProcessResult_flushAcknowledgement() { + public void testProcessResult_flushAcknowledgement() throws Exception { AutodetectResult result = mock(AutodetectResult.class); FlushAcknowledgement flushAcknowledgement = mock(FlushAcknowledgement.class); when(flushAcknowledgement.getId()).thenReturn(JOB_ID); @@ -219,13 +222,13 @@ public void testProcessResult_flushAcknowledgement() { processorUnderTest.processResult(result); assertTrue(processorUnderTest.isDeleteInterimRequired()); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(flushListener).acknowledgeFlush(flushAcknowledgement, null); verify(persister).commitResultWrites(JOB_ID); verify(bulkBuilder).executeRequest(); } - public void testProcessResult_flushAcknowledgementMustBeProcessedLast() { + public void testProcessResult_flushAcknowledgementMustBeProcessedLast() throws Exception { AutodetectResult result = mock(AutodetectResult.class); FlushAcknowledgement flushAcknowledgement = mock(FlushAcknowledgement.class); when(flushAcknowledgement.getId()).thenReturn(JOB_ID); @@ -238,14 +241,14 @@ public void testProcessResult_flushAcknowledgementMustBeProcessedLast() { assertTrue(processorUnderTest.isDeleteInterimRequired()); InOrder inOrder = inOrder(persister, bulkBuilder, flushListener); - inOrder.verify(persister).bulkPersisterBuilder(JOB_ID); - inOrder.verify(persister).persistCategoryDefinition(categoryDefinition); + inOrder.verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + inOrder.verify(persister).persistCategoryDefinition(eq(categoryDefinition), any()); inOrder.verify(bulkBuilder).executeRequest(); inOrder.verify(persister).commitResultWrites(JOB_ID); inOrder.verify(flushListener).acknowledgeFlush(flushAcknowledgement, null); } - public void testProcessResult_modelPlot() { + public void testProcessResult_modelPlot() throws Exception { AutodetectResult result = mock(AutodetectResult.class); ModelPlot modelPlot = mock(ModelPlot.class); when(result.getModelPlot()).thenReturn(modelPlot); @@ -253,11 +256,11 @@ public void testProcessResult_modelPlot() { processorUnderTest.setDeleteInterimRequired(false); processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(bulkBuilder).persistModelPlot(modelPlot); } - public void testProcessResult_modelSizeStats() { + public void testProcessResult_modelSizeStats() throws Exception { AutodetectResult result = mock(AutodetectResult.class); ModelSizeStats modelSizeStats = mock(ModelSizeStats.class); when(result.getModelSizeStats()).thenReturn(modelSizeStats); @@ -266,11 +269,11 @@ public void testProcessResult_modelSizeStats() { processorUnderTest.processResult(result); assertThat(processorUnderTest.modelSizeStats(), is(equalTo(modelSizeStats))); - verify(persister).bulkPersisterBuilder(JOB_ID); - verify(persister).persistModelSizeStats(modelSizeStats); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(persister).persistModelSizeStats(eq(modelSizeStats), any()); } - public void testProcessResult_modelSizeStatsWithMemoryStatusChanges() { + public void testProcessResult_modelSizeStatsWithMemoryStatusChanges() throws Exception { TimeValue delay = TimeValue.timeValueSeconds(5); // Set up schedule delay time when(threadPool.schedule(any(Runnable.class), any(TimeValue.class), anyString())) @@ -303,29 +306,30 @@ public void testProcessResult_modelSizeStatsWithMemoryStatusChanges() { when(result.getModelSizeStats()).thenReturn(modelSizeStats); processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(JOB_ID); - verify(persister, times(4)).persistModelSizeStats(any(ModelSizeStats.class)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(persister, times(4)).persistModelSizeStats(any(ModelSizeStats.class), any()); // We should have only fired two notifications: one for soft_limit and one for hard_limit verify(auditor).warning(JOB_ID, Messages.getMessage(Messages.JOB_AUDIT_MEMORY_STATUS_SOFT_LIMIT)); verify(auditor).error(JOB_ID, Messages.getMessage(Messages.JOB_AUDIT_MEMORY_STATUS_HARD_LIMIT, "512mb", "1kb")); } - public void testProcessResult_modelSnapshot() { + public void testProcessResult_modelSnapshot() throws Exception { AutodetectResult result = mock(AutodetectResult.class); ModelSnapshot modelSnapshot = new ModelSnapshot.Builder(JOB_ID) .setSnapshotId("a_snapshot_id") .setMinVersion(Version.CURRENT) .build(); when(result.getModelSnapshot()).thenReturn(modelSnapshot); + IndexResponse indexResponse = new IndexResponse(new ShardId("ml", "uid", 0), "1", 0L, 0L, 0L, true); - when(persister.persistModelSnapshot(any(), any())) - .thenReturn(new IndexResponse(new ShardId("ml", "uid", 0), "1", 0L, 0L, 0L, true)); + when(persister.persistModelSnapshot(any(), any(), any())) + .thenReturn(new BulkResponse(new BulkItemResponse[]{new BulkItemResponse(0, DocWriteRequest.OpType.INDEX, indexResponse)}, 0)); processorUnderTest.setDeleteInterimRequired(false); processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(JOB_ID); - verify(persister).persistModelSnapshot(modelSnapshot, WriteRequest.RefreshPolicy.IMMEDIATE); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(persister).persistModelSnapshot(eq(modelSnapshot), eq(WriteRequest.RefreshPolicy.IMMEDIATE), any()); UpdateJobAction.Request expectedJobUpdateRequest = UpdateJobAction.Request.internal(JOB_ID, new JobUpdate.Builder(JOB_ID).setModelSnapshotId("a_snapshot_id").build()); @@ -333,7 +337,7 @@ public void testProcessResult_modelSnapshot() { verify(client).execute(same(UpdateJobAction.INSTANCE), eq(expectedJobUpdateRequest), any()); } - public void testProcessResult_quantiles_givenRenormalizationIsEnabled() { + public void testProcessResult_quantiles_givenRenormalizationIsEnabled() throws Exception { AutodetectResult result = mock(AutodetectResult.class); Quantiles quantiles = mock(Quantiles.class); when(result.getQuantiles()).thenReturn(quantiles); @@ -342,15 +346,15 @@ public void testProcessResult_quantiles_givenRenormalizationIsEnabled() { processorUnderTest.setDeleteInterimRequired(false); processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(JOB_ID); - verify(persister).persistQuantiles(quantiles); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(persister).persistQuantiles(eq(quantiles), any()); verify(bulkBuilder).executeRequest(); verify(persister).commitResultWrites(JOB_ID); verify(renormalizer).isEnabled(); verify(renormalizer).renormalize(quantiles); } - public void testProcessResult_quantiles_givenRenormalizationIsDisabled() { + public void testProcessResult_quantiles_givenRenormalizationIsDisabled() throws Exception { AutodetectResult result = mock(AutodetectResult.class); Quantiles quantiles = mock(Quantiles.class); when(result.getQuantiles()).thenReturn(quantiles); @@ -359,8 +363,8 @@ public void testProcessResult_quantiles_givenRenormalizationIsDisabled() { processorUnderTest.setDeleteInterimRequired(false); processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(JOB_ID); - verify(persister).persistQuantiles(quantiles); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(persister).persistQuantiles(eq(quantiles), any()); verify(bulkBuilder).executeRequest(); verify(renormalizer).isEnabled(); } @@ -374,7 +378,7 @@ public void testAwaitCompletion() throws TimeoutException { assertThat(processorUnderTest.completionLatch.getCount(), is(equalTo(0L))); assertThat(processorUnderTest.updateModelSnapshotSemaphore.availablePermits(), is(equalTo(1))); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister).commitResultWrites(JOB_ID); verify(persister).commitStateWrites(JOB_ID); verify(renormalizer).waitUntilIdle(); @@ -389,12 +393,12 @@ public void testPersisterThrowingDoesntBlockProcessing() { when(process.isProcessAliveAfterWaiting()).thenReturn(true); when(process.readAutodetectResults()).thenReturn(Arrays.asList(autodetectResult, autodetectResult).iterator()); - doThrow(new ElasticsearchException("this test throws")).when(persister).persistModelSnapshot(any(), any()); + doThrow(new ElasticsearchException("this test throws")).when(persister).persistModelSnapshot(any(), any(), any()); processorUnderTest.process(); - verify(persister).bulkPersisterBuilder(JOB_ID); - verify(persister, times(2)).persistModelSnapshot(any(), eq(WriteRequest.RefreshPolicy.IMMEDIATE)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(persister, times(2)).persistModelSnapshot(any(), eq(WriteRequest.RefreshPolicy.IMMEDIATE), any()); } public void testParsingErrorSetsFailed() throws Exception { @@ -412,7 +416,7 @@ public void testParsingErrorSetsFailed() throws Exception { processorUnderTest.waitForFlushAcknowledgement(JOB_ID, Duration.of(300, ChronoUnit.SECONDS)); assertThat(flushAcknowledgement, is(nullValue())); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); } public void testKill() throws TimeoutException { @@ -425,7 +429,7 @@ public void testKill() throws TimeoutException { assertThat(processorUnderTest.completionLatch.getCount(), is(equalTo(0L))); assertThat(processorUnderTest.updateModelSnapshotSemaphore.availablePermits(), is(equalTo(1))); - verify(persister).bulkPersisterBuilder(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister).commitResultWrites(JOB_ID); verify(persister).commitStateWrites(JOB_ID); verify(renormalizer, never()).renormalize(any()); @@ -433,4 +437,5 @@ public void testKill() throws TimeoutException { verify(renormalizer).waitUntilIdle(); verify(flushListener).clear(); } + } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/CsvDataToProcessWriterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/CsvDataToProcessWriterTests.java index cf65eec4f04df..05604d0f876c2 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/CsvDataToProcessWriterTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/CsvDataToProcessWriterTests.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.ml.job.process.autodetect.writer; -import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.env.Environment; @@ -42,6 +41,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyLong; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -106,7 +106,7 @@ public void testWrite_GivenTimeFormatIsEpochAndDataIsValid() throws IOException expectedRecords.add(new String[] { "2", "2.0", "" }); assertWrittenRecordsEqualTo(expectedRecords); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenTimeFormatIsEpochAndCategorization() throws IOException { @@ -143,7 +143,7 @@ public void testWrite_GivenTimeFormatIsEpochAndCategorization() throws IOExcepti } assertWrittenRecordsEqualTo(expectedRecords); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenTimeFormatIsEpochAndTimestampsAreOutOfOrder() throws IOException { @@ -166,7 +166,7 @@ public void testWrite_GivenTimeFormatIsEpochAndTimestampsAreOutOfOrder() throws verify(dataCountsReporter, times(2)).reportOutOfOrderRecord(2); verify(dataCountsReporter, never()).reportLatestTimeIncrementalStats(anyLong()); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenTimeFormatIsEpochAndAllRecordsAreOutOfOrder() throws IOException { @@ -190,7 +190,7 @@ public void testWrite_GivenTimeFormatIsEpochAndAllRecordsAreOutOfOrder() throws verify(dataCountsReporter, times(2)).reportOutOfOrderRecord(2); verify(dataCountsReporter, times(2)).reportLatestTimeIncrementalStats(anyLong()); verify(dataCountsReporter, never()).reportRecordWritten(anyLong(), anyLong()); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenTimeFormatIsEpochAndSomeTimestampsWithinLatencySomeOutOfOrder() throws IOException { @@ -224,7 +224,7 @@ public void testWrite_GivenTimeFormatIsEpochAndSomeTimestampsWithinLatencySomeOu verify(dataCountsReporter, times(1)).reportOutOfOrderRecord(2); verify(dataCountsReporter, never()).reportLatestTimeIncrementalStats(anyLong()); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_NullByte() throws IOException { @@ -262,7 +262,7 @@ public void testWrite_NullByte() throws IOException { verify(dataCountsReporter, times(1)).reportRecordWritten(2, 3000); verify(dataCountsReporter, times(1)).reportRecordWritten(2, 4000); verify(dataCountsReporter, times(1)).reportDateParseError(2); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } @SuppressWarnings("unchecked") @@ -274,11 +274,7 @@ public void testWrite_EmptyInput() throws IOException { when(dataCountsReporter.incrementalStats()).thenReturn(new DataCounts("foo")); - doAnswer(invocation -> { - ActionListener listener = (ActionListener) invocation.getArguments()[0]; - listener.onResponse(true); - return null; - }).when(dataCountsReporter).finishReporting(any()); + doNothing().when(dataCountsReporter).finishReporting(); InputStream inputStream = createInputStream(""); CsvDataToProcessWriter writer = createWriter(); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/JsonDataToProcessWriterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/JsonDataToProcessWriterTests.java index f16b388edee6f..89ff28928fdae 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/JsonDataToProcessWriterTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/JsonDataToProcessWriterTests.java @@ -106,7 +106,7 @@ public void testWrite_GivenTimeFormatIsEpochAndDataIsValid() throws Exception { expectedRecords.add(new String[]{"2", "2.0", ""}); assertWrittenRecordsEqualTo(expectedRecords); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenTimeFormatIsEpochAndCategorization() throws Exception { @@ -142,7 +142,7 @@ public void testWrite_GivenTimeFormatIsEpochAndCategorization() throws Exception } assertWrittenRecordsEqualTo(expectedRecords); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenTimeFormatIsEpochAndTimestampsAreOutOfOrder() throws Exception { @@ -164,7 +164,7 @@ public void testWrite_GivenTimeFormatIsEpochAndTimestampsAreOutOfOrder() throws verify(dataCountsReporter, times(2)).reportOutOfOrderRecord(2); verify(dataCountsReporter, never()).reportLatestTimeIncrementalStats(anyLong()); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenTimeFormatIsEpochAndSomeTimestampsWithinLatencySomeOutOfOrder() throws Exception { @@ -195,7 +195,7 @@ public void testWrite_GivenTimeFormatIsEpochAndSomeTimestampsWithinLatencySomeOu verify(dataCountsReporter, times(1)).reportOutOfOrderRecord(2); verify(dataCountsReporter, never()).reportLatestTimeIncrementalStats(anyLong()); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenMalformedJsonWithoutNestedLevels() throws Exception { @@ -223,7 +223,7 @@ public void testWrite_GivenMalformedJsonWithoutNestedLevels() throws Exception { assertWrittenRecordsEqualTo(expectedRecords); verify(dataCountsReporter).reportMissingFields(1); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenMalformedJsonWithNestedLevels() @@ -251,7 +251,7 @@ public void testWrite_GivenMalformedJsonWithNestedLevels() expectedRecords.add(new String[]{"3", "3.0", ""}); assertWrittenRecordsEqualTo(expectedRecords); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenMalformedJsonThatNeverRecovers() @@ -293,7 +293,7 @@ public void testWrite_GivenJsonWithArrayField() throws Exception { expectedRecords.add(new String[]{"2", "2.0", ""}); assertWrittenRecordsEqualTo(expectedRecords); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_GivenJsonWithMissingFields() throws Exception { @@ -330,7 +330,7 @@ public void testWrite_GivenJsonWithMissingFields() throws Exception { verify(dataCountsReporter, times(1)).reportRecordWritten(1, 3000); verify(dataCountsReporter, times(1)).reportRecordWritten(1, 4000); verify(dataCountsReporter, times(1)).reportDateParseError(0); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } public void testWrite_Smile() throws Exception { @@ -367,7 +367,7 @@ public void testWrite_Smile() throws Exception { expectedRecords.add(new String[]{"2", "2.0", ""}); assertWrittenRecordsEqualTo(expectedRecords); - verify(dataCountsReporter).finishReporting(any()); + verify(dataCountsReporter).finishReporting(); } private static InputStream createInputStream(String input) { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/utils/persistence/ResultsPersisterServiceTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/utils/persistence/ResultsPersisterServiceTests.java new file mode 100644 index 0000000000000..9d5b922ccb0c6 --- /dev/null +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/utils/persistence/ResultsPersisterServiceTests.java @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.ml.utils.persistence; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.routing.OperationRouting; +import org.elasticsearch.cluster.service.ClusterApplierService; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.cluster.service.MasterService; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; +import org.elasticsearch.xpack.ml.inference.ingest.InferenceProcessor; +import org.mockito.ArgumentCaptor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ResultsPersisterServiceTests extends ESTestCase { + + private final String JOB_ID = "results_persister_test_job"; + private final Consumer NULL_MSG_HANDLER = (msg) -> {}; + + public void testBulkRequestChangeOnFailures() { + IndexRequest indexRequestSuccess = new IndexRequest("my-index").id("success").source(Collections.singletonMap("data", "success")); + IndexRequest indexRequestFail = new IndexRequest("my-index").id("fail").source(Collections.singletonMap("data", "fail")); + BulkItemResponse successItem = new BulkItemResponse(1, + DocWriteRequest.OpType.INDEX, + new IndexResponse(new ShardId(AnomalyDetectorsIndex.jobResultsIndexPrefix() + "shared", "uuid", 1), + indexRequestSuccess.id(), + 0, + 0, + 1, + true)); + BulkItemResponse failureItem = new BulkItemResponse(2, + DocWriteRequest.OpType.INDEX, + new BulkItemResponse.Failure("my-index", "fail", new Exception("boom"))); + BulkResponse withFailure = new BulkResponse(new BulkItemResponse[]{ failureItem, successItem }, 0L); + Client client = mockClientWithResponse(withFailure, new BulkResponse(new BulkItemResponse[0], 0L)); + + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(indexRequestFail); + bulkRequest.add(indexRequestSuccess); + + ResultsPersisterService resultsPersisterService = buildResultsPersisterService(client); + + resultsPersisterService.bulkIndexWithRetry(bulkRequest, JOB_ID, () -> true, NULL_MSG_HANDLER); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BulkRequest.class); + verify(client, times(2)).bulk(captor.capture()); + + List requests = captor.getAllValues(); + + assertThat(requests.get(0).numberOfActions(), equalTo(2)); + assertThat(requests.get(1).numberOfActions(), equalTo(1)); + } + + public void testBulkRequestDoesNotRetryWhenSupplierIsFalse() { + IndexRequest indexRequestSuccess = new IndexRequest("my-index").id("success").source(Collections.singletonMap("data", "success")); + IndexRequest indexRequestFail = new IndexRequest("my-index").id("fail").source(Collections.singletonMap("data", "fail")); + BulkItemResponse successItem = new BulkItemResponse(1, + DocWriteRequest.OpType.INDEX, + new IndexResponse(new ShardId(AnomalyDetectorsIndex.jobResultsIndexPrefix() + "shared", "uuid", 1), + indexRequestSuccess.id(), + 0, + 0, + 1, + true)); + BulkItemResponse failureItem = new BulkItemResponse(2, + DocWriteRequest.OpType.INDEX, + new BulkItemResponse.Failure("my-index", "fail", new Exception("boom"))); + BulkResponse withFailure = new BulkResponse(new BulkItemResponse[]{ failureItem, successItem }, 0L); + Client client = mockClientWithResponse(withFailure, new BulkResponse(new BulkItemResponse[0], 0L)); + + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(indexRequestFail); + bulkRequest.add(indexRequestSuccess); + + ResultsPersisterService resultsPersisterService = buildResultsPersisterService(client); + + expectThrows(ElasticsearchException.class, + () -> resultsPersisterService.bulkIndexWithRetry(bulkRequest, JOB_ID, () -> false, NULL_MSG_HANDLER)); + } + + public void testBulkRequestRetriesConfiguredAttemptNumber() { + IndexRequest indexRequestFail = new IndexRequest("my-index").id("fail").source(Collections.singletonMap("data", "fail")); + BulkItemResponse failureItem = new BulkItemResponse(2, + DocWriteRequest.OpType.INDEX, + new BulkItemResponse.Failure("my-index", "fail", new Exception("boom"))); + BulkResponse withFailure = new BulkResponse(new BulkItemResponse[]{ failureItem }, 0L); + Client client = mockClientWithResponse(withFailure); + + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(indexRequestFail); + + ResultsPersisterService resultsPersisterService = buildResultsPersisterService(client); + + resultsPersisterService.setMaxFailureRetries(1); + expectThrows(ElasticsearchException.class, + () -> resultsPersisterService.bulkIndexWithRetry(bulkRequest, JOB_ID, () -> true, NULL_MSG_HANDLER)); + verify(client, times(2)).bulk(any(BulkRequest.class)); + } + + public void testBulkRequestRetriesMsgHandlerIsCalled() { + IndexRequest indexRequestSuccess = new IndexRequest("my-index").id("success").source(Collections.singletonMap("data", "success")); + IndexRequest indexRequestFail = new IndexRequest("my-index").id("fail").source(Collections.singletonMap("data", "fail")); + BulkItemResponse successItem = new BulkItemResponse(1, + DocWriteRequest.OpType.INDEX, + new IndexResponse(new ShardId(AnomalyDetectorsIndex.jobResultsIndexPrefix() + "shared", "uuid", 1), + indexRequestSuccess.id(), + 0, + 0, + 1, + true)); + BulkItemResponse failureItem = new BulkItemResponse(2, + DocWriteRequest.OpType.INDEX, + new BulkItemResponse.Failure("my-index", "fail", new Exception("boom"))); + BulkResponse withFailure = new BulkResponse(new BulkItemResponse[]{ failureItem, successItem }, 0L); + Client client = mockClientWithResponse(withFailure, new BulkResponse(new BulkItemResponse[0], 0L)); + + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(indexRequestFail); + bulkRequest.add(indexRequestSuccess); + + ResultsPersisterService resultsPersisterService = buildResultsPersisterService(client); + AtomicReference msgHolder = new AtomicReference<>("not_called"); + + resultsPersisterService.bulkIndexWithRetry(bulkRequest, JOB_ID, () -> true, msgHolder::set); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BulkRequest.class); + verify(client, times(2)).bulk(captor.capture()); + + List requests = captor.getAllValues(); + + assertThat(requests.get(0).numberOfActions(), equalTo(2)); + assertThat(requests.get(1).numberOfActions(), equalTo(1)); + assertThat(msgHolder.get(), containsString("failed to index after [1] attempts. Will attempt again in")); + } + + @SuppressWarnings("unchecked") + private Client mockClientWithResponse(BulkResponse... responses) { + Client client = mock(Client.class); + ThreadPool threadPool = mock(ThreadPool.class); + when(client.threadPool()).thenReturn(threadPool); + when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); + List> futures = new ArrayList<>(responses.length - 1); + ActionFuture future1 = makeFuture(responses[0]); + for (int i = 1; i < responses.length; i++) { + futures.add(makeFuture(responses[i])); + } + when(client.bulk(any(BulkRequest.class))).thenReturn(future1, futures.toArray(ActionFuture[]::new)); + return client; + } + + @SuppressWarnings("unchecked") + private static ActionFuture makeFuture(BulkResponse response) { + ActionFuture future = mock(ActionFuture.class); + when(future.actionGet()).thenReturn(response); + return future; + } + + private ResultsPersisterService buildResultsPersisterService(Client client) { + ThreadPool tp = mock(ThreadPool.class); + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, + new HashSet<>(Arrays.asList(InferenceProcessor.MAX_INFERENCE_PROCESSORS, + MasterService.MASTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING, + OperationRouting.USE_ADAPTIVE_REPLICA_SELECTION_SETTING, + ClusterService.USER_DEFINED_META_DATA, + ResultsPersisterService.PERSIST_RESULTS_MAX_RETRIES, + ClusterApplierService.CLUSTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING))); + ClusterService clusterService = new ClusterService(Settings.EMPTY, clusterSettings, tp); + + return new ResultsPersisterService(client, clusterService, Settings.EMPTY); + } +} From 2e476de636f4e792110a2fe5bf8083f291f33ca7 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Thu, 12 Dec 2019 09:06:33 -0500 Subject: [PATCH 180/686] SingleBucket aggs need to reduce their bucket's pipelines first (#50103) When decoupling the pipeline reduction from regular agg reduction, MultiBucket aggs were modified to reduce their bucket's pipeline aggs first before reducing the sibling aggs. This modification was missed on SingleBucket aggs, meaning any SingleBucket would fail to reduce any pipeline sub-aggs --- .../InternalSingleBucketAggregation.java | 16 ++++ .../bucket/nested/NestedAggregatorTests.java | 86 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java index 0a34e7a92b8f1..bb0b2ad0a3050 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java @@ -109,6 +109,22 @@ public InternalAggregation reduce(List aggregations, Reduce return newAggregation(getName(), docCount, aggs); } + /** + * Unlike {@link InternalAggregation#reducePipelines(InternalAggregation, ReduceContext)}, a single-bucket + * agg needs to first reduce the aggs in it's bucket (and their parent pipelines) before allowing sibling pipelines + * to reduce + */ + @Override + public final InternalAggregation reducePipelines(InternalAggregation reducedAggs, ReduceContext reduceContext) { + assert reduceContext.isFinalReduce(); + List aggs = new ArrayList<>(); + for (Aggregation agg : getAggregations().asList()) { + aggs.add(((InternalAggregation)agg).reducePipelines((InternalAggregation)agg, reduceContext)); + } + InternalAggregations reducedSubAggs = new InternalAggregations(aggs); + return super.reducePipelines(create(reducedSubAggs), reduceContext); + } + @Override public Object getProperty(List path) { if (path.isEmpty()) { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregatorTests.java index f3c1ea7ca529a..4983575860f86 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregatorTests.java @@ -39,6 +39,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.lucene.search.Queries; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; @@ -47,11 +48,19 @@ import org.elasticsearch.index.mapper.TypeFieldMapper; import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.query.MatchAllQueryBuilder; +import org.elasticsearch.script.MockScriptEngine; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.script.ScriptModule; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.aggregations.AggregatorTestCase; import org.elasticsearch.search.aggregations.BucketOrder; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.terms.InternalTerms; +import org.elasticsearch.search.aggregations.bucket.terms.LongTerms; import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; @@ -62,12 +71,16 @@ import org.elasticsearch.search.aggregations.metrics.Min; import org.elasticsearch.search.aggregations.metrics.MinAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.SumAggregationBuilder; +import org.elasticsearch.search.aggregations.pipeline.BucketScriptPipelineAggregationBuilder; +import org.elasticsearch.search.aggregations.pipeline.InternalSimpleValue; import org.elasticsearch.search.aggregations.support.AggregationInspectionHelper; import org.elasticsearch.search.aggregations.support.ValueType; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -86,6 +99,7 @@ public class NestedAggregatorTests extends AggregatorTestCase { private static final String NESTED_AGG = "nestedAgg"; private static final String MAX_AGG_NAME = "maxAgg"; private static final String SUM_AGG_NAME = "sumAgg"; + private static final String INVERSE_SCRIPT = "inverse"; private final SeqNoFieldMapper.SequenceIDFields sequenceIDFields = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); @@ -99,6 +113,18 @@ protected Map getFieldAliases(MappedFieldType... fieldT Function.identity())); } + @Override + protected ScriptService getMockScriptService() { + Map, Object>> scripts = new HashMap<>(); + scripts.put(INVERSE_SCRIPT, vars -> -((Number) vars.get("_value")).doubleValue()); + MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, + scripts, + Collections.emptyMap()); + Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); + + return new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS); + } + public void testNoDocs() throws IOException { try (Directory directory = newDirectory()) { try (RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { @@ -709,6 +735,66 @@ public void testFieldAlias() throws IOException { } } + /** + * This tests to make sure pipeline aggs embedded under a SingleBucket agg (like nested) + * are properly reduced + */ + public void testNestedWithPipeline() throws IOException { + int numRootDocs = randomIntBetween(1, 20); + int expectedNestedDocs = 0; + double expectedMaxValue = Double.NEGATIVE_INFINITY; + try (Directory directory = newDirectory()) { + try (RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + for (int i = 0; i < numRootDocs; i++) { + List documents = new ArrayList<>(); + expectedMaxValue = Math.max(expectedMaxValue, + generateMaxDocs(documents, 1, i, NESTED_OBJECT, VALUE_FIELD_NAME)); + expectedNestedDocs += 1; + + Document document = new Document(); + document.add(new Field(IdFieldMapper.NAME, Uid.encodeId(Integer.toString(i)), IdFieldMapper.Defaults.FIELD_TYPE)); + document.add(new Field(TypeFieldMapper.NAME, "test", + TypeFieldMapper.Defaults.FIELD_TYPE)); + document.add(sequenceIDFields.primaryTerm); + documents.add(document); + iw.addDocuments(documents); + } + iw.commit(); + } + try (IndexReader indexReader = wrap(DirectoryReader.open(directory))) { + NestedAggregationBuilder nestedBuilder = new NestedAggregationBuilder(NESTED_AGG, NESTED_OBJECT) + .subAggregation(new TermsAggregationBuilder("terms", ValueType.NUMERIC).field(VALUE_FIELD_NAME) + .subAggregation(new MaxAggregationBuilder(MAX_AGG_NAME).field(VALUE_FIELD_NAME)) + .subAggregation(new BucketScriptPipelineAggregationBuilder("bucketscript", + Collections.singletonMap("_value", MAX_AGG_NAME), + new Script(ScriptType.INLINE, MockScriptEngine.NAME, INVERSE_SCRIPT, Collections.emptyMap())))); + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG); + fieldType.setName(VALUE_FIELD_NAME); + + InternalNested nested = searchAndReduce(newSearcher(indexReader, false, true), + new MatchAllDocsQuery(), nestedBuilder, fieldType); + + assertEquals(expectedNestedDocs, nested.getDocCount()); + assertEquals(NESTED_AGG, nested.getName()); + assertEquals(expectedNestedDocs, nested.getDocCount()); + + InternalTerms terms = (InternalTerms) nested.getProperty("terms"); + assertNotNull(terms); + + for (LongTerms.Bucket bucket : terms.getBuckets()) { + InternalMax max = (InternalMax) bucket.getAggregations().asMap().get(MAX_AGG_NAME); + InternalSimpleValue bucketScript = (InternalSimpleValue) bucket.getAggregations().asMap().get("bucketscript"); + assertNotNull(max); + assertNotNull(bucketScript); + assertEquals(max.getValue(), -bucketScript.getValue(), Double.MIN_VALUE); + } + + assertTrue(AggregationInspectionHelper.hasValue(nested)); + } + } + } + private double generateMaxDocs(List documents, int numNestedDocs, int id, String path, String fieldName) { return DoubleStream.of(generateDocuments(documents, numNestedDocs, id, path, fieldName)) .max().orElse(Double.NEGATIVE_INFINITY); From 4787977e72c3c27ad315bd179842be6893670888 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Thu, 12 Dec 2019 14:37:34 +0000 Subject: [PATCH 181/686] Add a Javadoc section to CONTRIBUTING.md (#49836) Introduce a set of contributor guidelines for when Javadoc should be written, and when it shouldn't. --- CONTRIBUTING.md | 112 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 102 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed96a2449568a..57021e3be565d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,7 +107,7 @@ and `JAVA11_HOME`, and `JAVA12_HOME` available so that the tests can pass. `jrunscript` for jdk distributions. Elasticsearch uses the Gradle wrapper for its build. You can execute Gradle -using the wrapper via the `gradlew` script on Unix systems or `gradlew.bat` +using the wrapper via the `gradlew` script on Unix systems or `gradlew.bat` script on Windows in the root of the repository. The examples below show the usage on Unix. @@ -157,9 +157,9 @@ For IntelliJ, go to For Eclipse, go to `Preferences->Java->Installed JREs` and add `-ea` to `VM Arguments`. -Some tests related to locale testing also require the flag +Some tests related to locale testing also require the flag `-Djava.locale.providers` to be set. Set the VM options/VM arguments for -IntelliJ or Eclipse like describe above to use +IntelliJ or Eclipse like describe above to use `-Djava.locale.providers=SPI,COMPAT`. ### Java Language Formatting Guidelines @@ -168,8 +168,8 @@ Java files in the Elasticsearch codebase are formatted with the Eclipse JDT formatter, using the [Spotless Gradle](https://github.com/diffplug/spotless/tree/master/plugin-gradle) plugin. This plugin is configured on a project-by-project basis, via -`build.gradle` in the root of the repository. So long as at least one -project is configured, the formatting check can be run explicitly with: +`build.gradle` in the root of the repository. The formatting check can be +run explicitly with: ./gradlew spotlessJavaCheck @@ -205,7 +205,7 @@ Please follow these formatting guidelines: these are intended for use in documentation, so please make it clear what you have done, and only do this where the benefit clearly outweighs the decrease in consistency. -* Note that JavaDoc and block comments i.e. `/* ... */` are not formatted, +* Note that Javadoc and block comments i.e. `/* ... */` are not formatted, but line comments i.e `// ...` are. * There is an implicit rule that negative boolean expressions should use the form `foo == false` instead of `!foo` for better readability of the @@ -231,15 +231,107 @@ from the command line. Sometimes Spotless will report a "misbehaving rule which can't make up its mind" and will recommend enabling the `paddedCell()` setting. If you -enabled this settings and run the format check again, +enabled this setting and run the format check again, Spotless will write files to `$PROJECT/build/spotless-diagnose-java/` to aid diagnosis. It writes different copies of the formatted files, so that you can see how they differ and infer what is the problem. -The `paddedCell()` option is disabled for normal operation in order to -detect any misbehaviour. You can enabled the option from the command line -by running Gradle with `-Dspotless.paddedcell`. +The `paddedCell()` option is disabled for normal operation so that any +misbehaviour is detected, and not just suppressed. You can enabled the +option from the command line by running Gradle with `-Dspotless.paddedcell`. + +### Javadoc + +Good Javadoc can help with navigating and understanding code. Elasticsearch +has some guidelines around when to write Javadoc and when not to, but note +that we don't want to be overly prescriptive. The intent of these guidelines +is to be helpful, not to turn writing code into a chore. + +#### The short version + + 1. Always add Javadoc to new code. + 2. Add Javadoc to existing code if you can. + 3. Document the "why", not the "how", unless that's important to the + "why". + 4. Don't document anything trivial or obvious (e.g. getters and + setters). In other words, the Javadoc should add some value. + +#### The long version + + 1. If you add a new Java package, please also add package-level + Javadoc that explains what the package is for. This can just be a + reference to a more foundational / parent package if appropriate. An + example would be a package hierarchy for a new feature or plugin - + the package docs could explain the purpose of the feature, any + caveats, and possibly some examples of configuration and usage. + 2. New classes and interfaces must have class-level Javadoc that + describes their purpose. There are a lot of classes in the + Elasticsearch repository, and it's easier to navigate when you + can quickly find out what is the purpose of a class. This doesn't + apply to inner classes or interfaces, unless you expect them to be + explicitly used outside their parent class. + 3. New public methods must have Javadoc, because they form part of the + contract between the class and its consumers. Similarly, new abstract + methods must have Javadoc because they are part of the contract + between a class and its subclasses. It's important that contributors + know why they need to implement a method, and the Javadoc should make + this clear. You don't need to document a method if it's overriding an + abstract method (either from an abstract superclass or an interface), + unless your implementation is doing something "unexpected" e.g. deviating + from the intent of the original method. + 4. Following on from the above point, please add docs to existing public + methods if you are editing them, or to abstract methods if you can. + 5. Non-public, non-abstract methods don't require Javadoc, but if you feel + that adding some would make it easier for other developers to + understand the code, or why it's written in a particular way, then please + do so. + 6. Properties don't need to have Javadoc, but please add some if there's + something useful to say. + 7. Javadoc should not go into low-level implementation details unless + this is critical to understanding the code e.g. documenting the + subtleties of the implementation of a private method. The point here + is that implementations will change over time, and the Javadoc is + less likely to become out-of-date if it only talks about the what is + the purpose of the code, not what it does. + 8. Examples in Javadoc can be very useful, so feel free to add some if + you can reasonably do so i.e. if it takes a whole page of code to set + up an example, then Javadoc probably isn't the right place for it. + Longer or more elaborate examples are probably better suited + to the package docs. + 9. Test methods are a good place to add Javadoc, because you can use it + to succinctly describe e.g. preconditions, actions and expectations + of the test, more easily that just using the test name alone. Please + consider documenting your tests in this way. + 10. Sometimes you shouldn't add Javadoc: + 1. Where it adds no value, for example where a method's + implementation is trivial such as with getters and setters, or a + method just delegates to another object. + 2. However, you should still add Javadoc if there are caveats around + calling a method that are not immediately obvious from reading the + method's implementation in isolation. + 3. You can omit Javadoc for simple classes, e.g. where they are a + simple container for some data. However, please consider whether a + reader might still benefit from some additional background, for + example about why the class exists at all. + 11. Not all comments need to be Javadoc. Sometimes it will make more + sense to add comments in a method's body, for example due to important + implementation decisions or "gotchas". As a general guide, if some + information forms part of the contract between a method and its callers, + then it should go in the Javadoc, otherwise you might consider using + regular comments in the code. Remember as well that Elasticsearch + has extensive [user documentation](./docs), and it is not the role + of Javadoc to replace that. + 12. Please still try to make class, method or variable names as + descriptive and concise as possible, as opposed to relying solely on + Javadoc to describe something. + 13. Use `@link` and `@see` to add references, either to related + resources in the codebase or to relevant external resources. + 14. If you need help writing Javadoc, just ask! + +Finally, use your judgement! Base your decisions on what will help other +developers - including yourself, when you come back to some code +3 months in the future, having forgotten how it works. ### License Headers From b6ffc9f8843c93ab96f7a9cdc8f082edd1f52e85 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Thu, 12 Dec 2019 09:39:06 -0500 Subject: [PATCH 182/686] [DOCS] Reformat lowercase token filter docs (#49935) --- .../lowercase-tokenfilter.asciidoc | 134 ++++++++++++++++-- 1 file changed, 122 insertions(+), 12 deletions(-) diff --git a/docs/reference/analysis/tokenfilters/lowercase-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/lowercase-tokenfilter.asciidoc index f04cea237fa99..6a1ef08c0b12d 100644 --- a/docs/reference/analysis/tokenfilters/lowercase-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/lowercase-tokenfilter.asciidoc @@ -1,25 +1,135 @@ [[analysis-lowercase-tokenfilter]] -=== Lowercase Token Filter +=== Lowercase token filter +++++ +Lowercase +++++ -A token filter of type `lowercase` that normalizes token text to lower -case. +Changes token text to lowercase. For example, you can use the `lowercase` filter +to change `THE Lazy DoG` to `the lazy dog`. -Lowercase token filter supports Greek, Irish, and Turkish lowercase token -filters through the `language` parameter. Below is a usage example in a -custom analyzer +In addition to a default filter, the `lowercase` token filter provides access to +Lucene's language-specific lowercase filters for Greek, Irish, and Turkish. + +[[analysis-lowercase-tokenfilter-analyze-ex]] +==== Example + +The following <> request uses the default +`lowercase` filter to change the `THE Quick FoX JUMPs` to lowercase: + +[source,console] +-------------------------------------------------- +GET _analyze +{ + "tokenizer" : "standard", + "filter" : ["lowercase"], + "text" : "THE Quick FoX JUMPs" +} +-------------------------------------------------- + +The filter produces the following tokens: + +[source,text] +-------------------------------------------------- +[ the, quick, fox, jumps ] +-------------------------------------------------- + +///////////////////// +[source,console-result] +-------------------------------------------------- +{ + "tokens" : [ + { + "token" : "the", + "start_offset" : 0, + "end_offset" : 3, + "type" : "", + "position" : 0 + }, + { + "token" : "quick", + "start_offset" : 4, + "end_offset" : 9, + "type" : "", + "position" : 1 + }, + { + "token" : "fox", + "start_offset" : 10, + "end_offset" : 13, + "type" : "", + "position" : 2 + }, + { + "token" : "jumps", + "start_offset" : 14, + "end_offset" : 19, + "type" : "", + "position" : 3 + } + ] +} +-------------------------------------------------- +///////////////////// + +[[analysis-lowercase-tokenfilter-analyzer-ex]] +==== Add to an analyzer + +The following <> request uses the +`lowercase` filter to configure a new +<>. [source,console] -------------------------------------------------- -PUT /lowercase_example +PUT lowercase_example +{ + "settings" : { + "analysis" : { + "analyzer" : { + "whitespace_lowercase" : { + "tokenizer" : "whitespace", + "filter" : ["lowercase"] + } + } + } + } +} +-------------------------------------------------- + +[[analysis-lowercase-tokenfilter-configure-parms]] +==== Configurable parameters + +`language`:: ++ +-- +(Optional, string) +Language-specific lowercase token filter to use. Valid values include: + +`greek`::: Uses Lucene's https://lucene.apache.org/core/{lucene_version_path}/analyzers-common/org/apache/lucene/analysis/el/GreekLowerCaseFilter.html[GreekLowerCaseFilter] + +`irish`::: Uses Lucene's http://lucene.apache.org/core/{lucene_version_path}/analyzers-common/org/apache/lucene/analysis/ga/IrishLowerCaseFilter.html[IrishLowerCaseFilter] + +`turkish`::: Uses Lucene's https://lucene.apache.org/core/{lucene_version_path}/analyzers-common/org/apache/lucene/analysis/tr/TurkishLowerCaseFilter.html[TurkishLowerCaseFilter] + +If not specified, defaults to Lucene's https://lucene.apache.org/core/{lucene_version_path}/analyzers-common/org/apache/lucene/analysis/core/LowerCaseFilter.html[LowerCaseFilter]. +-- + +[[analysis-lowercase-tokenfilter-customize]] +==== Customize + +To customize the `lowercase` filter, duplicate it to create the basis +for a new custom token filter. You can modify the filter using its configurable +parameters. + +For example, the following request creates a custom `lowercase` filter for the +Greek language: + +[source,console] +-------------------------------------------------- +PUT custom_lowercase_example { "settings": { "analysis": { "analyzer": { - "standard_lowercase_example": { - "type": "custom", - "tokenizer": "standard", - "filter": ["lowercase"] - }, "greek_lowercase_example": { "type": "custom", "tokenizer": "standard", From dbcab441c28d627b8a99d1a13f34b3801d97db7b Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Thu, 12 Dec 2019 10:19:39 -0500 Subject: [PATCH 183/686] [ML][Inference] Simplify inference processor options (#50105) * [ML][Inference] Simplify inference processor options * addressing pr comments --- .../ml/action/InternalInferModelAction.java | 2 +- .../ClassificationInferenceResults.java | 63 +++----- .../inference/results/InferenceResults.java | 6 +- .../results/RawInferenceResults.java | 13 +- .../results/RegressionInferenceResults.java | 30 ++-- .../results/SingleValueInferenceResults.java | 13 -- .../trainedmodel/ClassificationConfig.java | 38 +++-- .../trainedmodel/RegressionConfig.java | 32 +++- .../trainedmodel/ensemble/Ensemble.java | 5 +- .../ml/inference/trainedmodel/tree/Tree.java | 4 +- .../ClassificationInferenceResultsTests.java | 29 ++-- .../RegressionInferenceResultsTests.java | 9 +- .../ClassificationConfigTests.java | 9 +- .../trainedmodel/RegressionConfigTests.java | 11 +- .../trainedmodel/ensemble/EnsembleTests.java | 10 +- .../trainedmodel/tree/TreeTests.java | 12 +- .../ml/integration/InferenceIngestIT.java | 29 ++-- .../inference/ingest/InferenceProcessor.java | 35 ++-- .../MachineLearningLicensingTests.java | 8 +- .../ingest/InferenceProcessorTests.java | 150 +++++------------- .../loadingservice/LocalModelTests.java | 2 +- .../integration/ModelInferenceActionIT.java | 13 +- 22 files changed, 226 insertions(+), 297 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InternalInferModelAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InternalInferModelAction.java index 0ff458994719d..556fb2b4f0241 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InternalInferModelAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InternalInferModelAction.java @@ -41,7 +41,7 @@ public static class Request extends ActionRequest { private final boolean previouslyLicensed; public Request(String modelId, boolean previouslyLicensed) { - this(modelId, Collections.emptyList(), new RegressionConfig(), previouslyLicensed); + this(modelId, Collections.emptyList(), RegressionConfig.EMPTY_PARAMS, previouslyLicensed); } public Request(String modelId, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResults.java index 4df54e3fa9eca..526b37314b56b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResults.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResults.java @@ -9,8 +9,6 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.common.xcontent.ToXContentObject; -import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.ingest.IngestDocument; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ClassificationConfig; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; @@ -27,22 +25,31 @@ public class ClassificationInferenceResults extends SingleValueInferenceResults { public static final String NAME = "classification"; - public static final ParseField CLASSIFICATION_LABEL = new ParseField("classification_label"); - public static final ParseField TOP_CLASSES = new ParseField("top_classes"); - + + private final String topNumClassesField; + private final String resultsField; private final String classificationLabel; private final List topClasses; - public ClassificationInferenceResults(double value, String classificationLabel, List topClasses) { + public ClassificationInferenceResults(double value, + String classificationLabel, + List topClasses, + InferenceConfig config) { super(value); + assert config instanceof ClassificationConfig; + ClassificationConfig classificationConfig = (ClassificationConfig)config; this.classificationLabel = classificationLabel; this.topClasses = topClasses == null ? Collections.emptyList() : Collections.unmodifiableList(topClasses); + this.topNumClassesField = classificationConfig.getTopClassesResultsField(); + this.resultsField = classificationConfig.getResultsField(); } public ClassificationInferenceResults(StreamInput in) throws IOException { super(in); this.classificationLabel = in.readOptionalString(); this.topClasses = Collections.unmodifiableList(in.readList(TopClassEntry::new)); + this.topNumClassesField = in.readString(); + this.resultsField = in.readString(); } public String getClassificationLabel() { @@ -58,17 +65,8 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeOptionalString(classificationLabel); out.writeCollection(topClasses); - } - - @Override - XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException { - if (classificationLabel != null) { - builder.field(CLASSIFICATION_LABEL.getPreferredName(), classificationLabel); - } - if (topClasses.isEmpty() == false) { - builder.field(TOP_CLASSES.getPreferredName(), topClasses); - } - return builder; + out.writeString(topNumClassesField); + out.writeString(resultsField); } @Override @@ -78,12 +76,14 @@ public boolean equals(Object object) { ClassificationInferenceResults that = (ClassificationInferenceResults) object; return Objects.equals(value(), that.value()) && Objects.equals(classificationLabel, that.classificationLabel) && + Objects.equals(resultsField, that.resultsField) && + Objects.equals(topNumClassesField, that.topNumClassesField) && Objects.equals(topClasses, that.topClasses); } @Override public int hashCode() { - return Objects.hash(value(), classificationLabel, topClasses); + return Objects.hash(value(), classificationLabel, topClasses, resultsField, topNumClassesField); } @Override @@ -92,14 +92,12 @@ public String valueAsString() { } @Override - public void writeResult(IngestDocument document, String resultField, InferenceConfig config) { - assert config instanceof ClassificationConfig; - ClassificationConfig classificationConfig = (ClassificationConfig)config; + public void writeResult(IngestDocument document, String parentResultField) { ExceptionsHelper.requireNonNull(document, "document"); - ExceptionsHelper.requireNonNull(resultField, "resultField"); - document.setFieldValue(resultField, valueAsString()); - if (topClasses.isEmpty() == false) { - document.setFieldValue(classificationConfig.getTopClassesResultsField(), + ExceptionsHelper.requireNonNull(parentResultField, "parentResultField"); + document.setFieldValue(parentResultField + "." + this.resultsField, valueAsString()); + if (topClasses.size() > 0) { + document.setFieldValue(parentResultField + "." + topNumClassesField, topClasses.stream().map(TopClassEntry::asValueMap).collect(Collectors.toList())); } } @@ -109,12 +107,8 @@ public String getWriteableName() { return NAME; } - @Override - public String getName() { - return NAME; - } - public static class TopClassEntry implements ToXContentObject, Writeable { + public static class TopClassEntry implements Writeable { public final ParseField CLASSIFICATION = new ParseField("classification"); public final ParseField PROBABILITY = new ParseField("probability"); @@ -153,15 +147,6 @@ public void writeTo(StreamOutput out) throws IOException { out.writeDouble(probability); } - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(CLASSIFICATION.getPreferredName(), classification); - builder.field(PROBABILITY.getPreferredName(), probability); - builder.endObject(); - return builder; - } - @Override public boolean equals(Object object) { if (object == this) { return true; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/InferenceResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/InferenceResults.java index 357fdcda386df..a0fe44a13d1b1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/InferenceResults.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/InferenceResults.java @@ -7,11 +7,9 @@ import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.ingest.IngestDocument; -import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; -import org.elasticsearch.xpack.core.ml.utils.NamedXContentObject; -public interface InferenceResults extends NamedXContentObject, NamedWriteable { +public interface InferenceResults extends NamedWriteable { - void writeResult(IngestDocument document, String resultField, InferenceConfig config); + void writeResult(IngestDocument document, String parentResultField); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RawInferenceResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RawInferenceResults.java index bb72483fe4878..6525908af3acd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RawInferenceResults.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RawInferenceResults.java @@ -7,9 +7,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.ingest.IngestDocument; -import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; import java.io.IOException; import java.util.Objects; @@ -31,11 +29,6 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); } - @Override - XContentBuilder innerToXContent(XContentBuilder builder, Params params) { - return builder; - } - @Override public boolean equals(Object object) { if (object == this) { return true; } @@ -50,7 +43,7 @@ public int hashCode() { } @Override - public void writeResult(IngestDocument document, String resultField, InferenceConfig config) { + public void writeResult(IngestDocument document, String parentResultField) { throw new UnsupportedOperationException("[raw] does not support writing inference results"); } @@ -59,8 +52,4 @@ public String getWriteableName() { return NAME; } - @Override - public String getName() { - return NAME; - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResults.java index 0fb931cd4a251..8aade6337f5a6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResults.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResults.java @@ -7,9 +7,9 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.ingest.IngestDocument; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; +import org.elasticsearch.xpack.core.ml.inference.trainedmodel.RegressionConfig; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.io.IOException; @@ -19,22 +19,24 @@ public class RegressionInferenceResults extends SingleValueInferenceResults { public static final String NAME = "regression"; - public RegressionInferenceResults(double value) { + private final String resultsField; + + public RegressionInferenceResults(double value, InferenceConfig config) { super(value); + assert config instanceof RegressionConfig; + RegressionConfig regressionConfig = (RegressionConfig)config; + this.resultsField = regressionConfig.getResultsField(); } public RegressionInferenceResults(StreamInput in) throws IOException { super(in.readDouble()); + this.resultsField = in.readString(); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - } - - @Override - XContentBuilder innerToXContent(XContentBuilder builder, Params params) { - return builder; + out.writeString(resultsField); } @Override @@ -42,19 +44,19 @@ public boolean equals(Object object) { if (object == this) { return true; } if (object == null || getClass() != object.getClass()) { return false; } RegressionInferenceResults that = (RegressionInferenceResults) object; - return Objects.equals(value(), that.value()); + return Objects.equals(value(), that.value()) && Objects.equals(this.resultsField, that.resultsField); } @Override public int hashCode() { - return Objects.hash(value()); + return Objects.hash(value(), resultsField); } @Override - public void writeResult(IngestDocument document, String resultField, InferenceConfig config) { + public void writeResult(IngestDocument document, String parentResultField) { ExceptionsHelper.requireNonNull(document, "document"); - ExceptionsHelper.requireNonNull(resultField, "resultField"); - document.setFieldValue(resultField, value()); + ExceptionsHelper.requireNonNull(parentResultField, "parentResultField"); + document.setFieldValue(parentResultField + "." + this.resultsField, value()); } @Override @@ -62,8 +64,4 @@ public String getWriteableName() { return NAME; } - @Override - public String getName() { - return NAME; - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/SingleValueInferenceResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/SingleValueInferenceResults.java index 2905a6679584c..a93f2b0f56b6a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/SingleValueInferenceResults.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/SingleValueInferenceResults.java @@ -5,17 +5,13 @@ */ package org.elasticsearch.xpack.core.ml.inference.results; -import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; public abstract class SingleValueInferenceResults implements InferenceResults { - public final ParseField VALUE = new ParseField("value"); - private final double value; SingleValueInferenceResults(StreamInput in) throws IOException { @@ -39,13 +35,4 @@ public void writeTo(StreamOutput out) throws IOException { out.writeDouble(value); } - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(VALUE.getPreferredName(), value); - innerToXContent(builder, params); - builder.endObject(); - return builder; - } - - abstract XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfig.java index 3923ef9313190..28e716d569fb1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfig.java @@ -21,38 +21,44 @@ public class ClassificationConfig implements InferenceConfig { public static final String NAME = "classification"; - public static final String DEFAULT_TOP_CLASSES_RESULT_FIELD = "top_classes"; - public static final ParseField NUM_TOP_CLASSES = new ParseField("num_top_classes"); - public static final ParseField TOP_CLASSES_RESULT_FIELD = new ParseField("top_classes_result_field"); + public static final String DEFAULT_TOP_CLASSES_RESULTS_FIELD = "top_classes"; + private static final String DEFAULT_RESULTS_FIELD = "predicted_value"; + public static final ParseField RESULTS_FIELD = new ParseField("results_field"); + public static final ParseField NUM_TOP_CLASSES = new ParseField("num_top_classes"); + public static final ParseField TOP_CLASSES_RESULTS_FIELD = new ParseField("top_classes_results_field"); private static final Version MIN_SUPPORTED_VERSION = Version.V_7_6_0; - public static ClassificationConfig EMPTY_PARAMS = new ClassificationConfig(0, DEFAULT_TOP_CLASSES_RESULT_FIELD); + public static ClassificationConfig EMPTY_PARAMS = new ClassificationConfig(0, DEFAULT_RESULTS_FIELD, DEFAULT_TOP_CLASSES_RESULTS_FIELD); private final int numTopClasses; private final String topClassesResultsField; + private final String resultsField; public static ClassificationConfig fromMap(Map map) { Map options = new HashMap<>(map); Integer numTopClasses = (Integer)options.remove(NUM_TOP_CLASSES.getPreferredName()); - String topClassesResultsField = (String)options.remove(TOP_CLASSES_RESULT_FIELD.getPreferredName()); + String topClassesResultsField = (String)options.remove(TOP_CLASSES_RESULTS_FIELD.getPreferredName()); + String resultsField = (String)options.remove(RESULTS_FIELD.getPreferredName()); if (options.isEmpty() == false) { throw ExceptionsHelper.badRequestException("Unrecognized fields {}.", options.keySet()); } - return new ClassificationConfig(numTopClasses, topClassesResultsField); + return new ClassificationConfig(numTopClasses, resultsField, topClassesResultsField); } public ClassificationConfig(Integer numTopClasses) { - this(numTopClasses, null); + this(numTopClasses, null, null); } - public ClassificationConfig(Integer numTopClasses, String topClassesResultsField) { + public ClassificationConfig(Integer numTopClasses, String resultsField, String topClassesResultsField) { this.numTopClasses = numTopClasses == null ? 0 : numTopClasses; - this.topClassesResultsField = topClassesResultsField == null ? DEFAULT_TOP_CLASSES_RESULT_FIELD : topClassesResultsField; + this.topClassesResultsField = topClassesResultsField == null ? DEFAULT_TOP_CLASSES_RESULTS_FIELD : topClassesResultsField; + this.resultsField = resultsField == null ? DEFAULT_RESULTS_FIELD : resultsField; } public ClassificationConfig(StreamInput in) throws IOException { this.numTopClasses = in.readInt(); this.topClassesResultsField = in.readString(); + this.resultsField = in.readString(); } public int getNumTopClasses() { @@ -63,10 +69,15 @@ public String getTopClassesResultsField() { return topClassesResultsField; } + public String getResultsField() { + return resultsField; + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeInt(numTopClasses); out.writeString(topClassesResultsField); + out.writeString(resultsField); } @Override @@ -74,12 +85,14 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ClassificationConfig that = (ClassificationConfig) o; - return Objects.equals(numTopClasses, that.numTopClasses) && Objects.equals(topClassesResultsField, that.topClassesResultsField); + return Objects.equals(numTopClasses, that.numTopClasses) && + Objects.equals(topClassesResultsField, that.topClassesResultsField) && + Objects.equals(resultsField, that.resultsField); } @Override public int hashCode() { - return Objects.hash(numTopClasses, topClassesResultsField); + return Objects.hash(numTopClasses, topClassesResultsField, resultsField); } @Override @@ -88,7 +101,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (numTopClasses != 0) { builder.field(NUM_TOP_CLASSES.getPreferredName(), numTopClasses); } - builder.field(TOP_CLASSES_RESULT_FIELD.getPreferredName(), topClassesResultsField); + builder.field(TOP_CLASSES_RESULTS_FIELD.getPreferredName(), topClassesResultsField); + builder.field(RESULTS_FIELD.getPreferredName(), resultsField); builder.endObject(); return builder; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/RegressionConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/RegressionConfig.java index 6dd03e87747e7..f42ef20a94337 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/RegressionConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/RegressionConfig.java @@ -6,12 +6,14 @@ package org.elasticsearch.xpack.core.ml.inference.trainedmodel; import org.elasticsearch.Version; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -19,18 +21,32 @@ public class RegressionConfig implements InferenceConfig { public static final String NAME = "regression"; private static final Version MIN_SUPPORTED_VERSION = Version.V_7_6_0; + public static final ParseField RESULTS_FIELD = new ParseField("results_field"); + private static final String DEFAULT_RESULTS_FIELD = "predicted_value"; + + public static RegressionConfig EMPTY_PARAMS = new RegressionConfig(DEFAULT_RESULTS_FIELD); public static RegressionConfig fromMap(Map map) { - if (map.isEmpty() == false) { + Map options = new HashMap<>(map); + String resultsField = (String)options.remove(RESULTS_FIELD.getPreferredName()); + if (options.isEmpty() == false) { throw ExceptionsHelper.badRequestException("Unrecognized fields {}.", map.keySet()); } - return new RegressionConfig(); + return new RegressionConfig(resultsField); } - public RegressionConfig() { + private final String resultsField; + + public RegressionConfig(String resultsField) { + this.resultsField = resultsField == null ? DEFAULT_RESULTS_FIELD : resultsField; } - public RegressionConfig(StreamInput in) { + public RegressionConfig(StreamInput in) throws IOException { + this.resultsField = in.readString(); + } + + public String getResultsField() { + return resultsField; } @Override @@ -40,6 +56,7 @@ public String getWriteableName() { @Override public void writeTo(StreamOutput out) throws IOException { + out.writeString(resultsField); } @Override @@ -50,6 +67,7 @@ public String getName() { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); + builder.field(RESULTS_FIELD.getPreferredName(), resultsField); builder.endObject(); return builder; } @@ -58,13 +76,13 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - - return true; + RegressionConfig that = (RegressionConfig)o; + return Objects.equals(this.resultsField, that.resultsField); } @Override public int hashCode() { - return Objects.hash(NAME); + return Objects.hash(resultsField); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/Ensemble.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/Ensemble.java index a59f1a1c245d9..c9bde54c460cd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/Ensemble.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/Ensemble.java @@ -150,7 +150,7 @@ private InferenceResults buildResults(List processedInferences, Inferenc } switch(targetType) { case REGRESSION: - return new RegressionInferenceResults(outputAggregator.aggregate(processedInferences)); + return new RegressionInferenceResults(outputAggregator.aggregate(processedInferences), config); case CLASSIFICATION: ClassificationConfig classificationConfig = (ClassificationConfig) config; List topClasses = InferenceHelpers.topClasses( @@ -160,7 +160,8 @@ private InferenceResults buildResults(List processedInferences, Inferenc double value = outputAggregator.aggregate(processedInferences); return new ClassificationInferenceResults(outputAggregator.aggregate(processedInferences), classificationLabel(value, classificationLabels), - topClasses); + topClasses, + config); default: throw new UnsupportedOperationException("unsupported target_type [" + targetType + "] for inference on ensemble model"); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/Tree.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/Tree.java index 1408b17a0691a..b137c8f28d58e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/Tree.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/Tree.java @@ -153,9 +153,9 @@ private InferenceResults buildResult(Double value, InferenceConfig config) { classificationProbability(value), classificationLabels, classificationConfig.getNumTopClasses()); - return new ClassificationInferenceResults(value, classificationLabel(value, classificationLabels), topClasses); + return new ClassificationInferenceResults(value, classificationLabel(value, classificationLabels), topClasses, config); case REGRESSION: - return new RegressionInferenceResults(value); + return new RegressionInferenceResults(value, config); default: throw new UnsupportedOperationException("unsupported target_type [" + targetType + "] for inference on tree model"); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResultsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResultsTests.java index cc37e253806b0..3ccfb54bd6924 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResultsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/ClassificationInferenceResultsTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.ingest.IngestDocument; import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ClassificationConfig; +import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ClassificationConfigTests; import java.util.Arrays; import java.util.Collections; @@ -28,7 +29,8 @@ public static ClassificationInferenceResults createRandomResults() { randomBoolean() ? null : Stream.generate(ClassificationInferenceResultsTests::createRandomClassEntry) .limit(randomIntBetween(0, 10)) - .collect(Collectors.toList())); + .collect(Collectors.toList()), + ClassificationConfigTests.randomClassificationConfig()); } private static ClassificationInferenceResults.TopClassEntry createRandomClassEntry() { @@ -36,19 +38,23 @@ private static ClassificationInferenceResults.TopClassEntry createRandomClassEnt } public void testWriteResultsWithClassificationLabel() { - ClassificationInferenceResults result = new ClassificationInferenceResults(1.0, "foo", Collections.emptyList()); + ClassificationInferenceResults result = + new ClassificationInferenceResults(1.0, "foo", Collections.emptyList(), ClassificationConfig.EMPTY_PARAMS); IngestDocument document = new IngestDocument(new HashMap<>(), new HashMap<>()); - result.writeResult(document, "result_field", ClassificationConfig.EMPTY_PARAMS); + result.writeResult(document, "result_field"); - assertThat(document.getFieldValue("result_field", String.class), equalTo("foo")); + assertThat(document.getFieldValue("result_field.predicted_value", String.class), equalTo("foo")); } public void testWriteResultsWithoutClassificationLabel() { - ClassificationInferenceResults result = new ClassificationInferenceResults(1.0, null, Collections.emptyList()); + ClassificationInferenceResults result = new ClassificationInferenceResults(1.0, + null, + Collections.emptyList(), + ClassificationConfig.EMPTY_PARAMS); IngestDocument document = new IngestDocument(new HashMap<>(), new HashMap<>()); - result.writeResult(document, "result_field", ClassificationConfig.EMPTY_PARAMS); + result.writeResult(document, "result_field"); - assertThat(document.getFieldValue("result_field", String.class), equalTo("1.0")); + assertThat(document.getFieldValue("result_field.predicted_value", String.class), equalTo("1.0")); } @SuppressWarnings("unchecked") @@ -59,11 +65,12 @@ public void testWriteResultsWithTopClasses() { new ClassificationInferenceResults.TopClassEntry("baz", 0.1)); ClassificationInferenceResults result = new ClassificationInferenceResults(1.0, "foo", - entries); + entries, + new ClassificationConfig(3, "my_results", "bar")); IngestDocument document = new IngestDocument(new HashMap<>(), new HashMap<>()); - result.writeResult(document, "result_field", new ClassificationConfig(3, "bar")); + result.writeResult(document, "result_field"); - List list = document.getFieldValue("bar", List.class); + List list = document.getFieldValue("result_field.bar", List.class); assertThat(list.size(), equalTo(3)); for(int i = 0; i < 3; i++) { @@ -71,7 +78,7 @@ public void testWriteResultsWithTopClasses() { assertThat(map, equalTo(entries.get(i).asValueMap())); } - assertThat(document.getFieldValue("result_field", String.class), equalTo("foo")); + assertThat(document.getFieldValue("result_field.my_results", String.class), equalTo("foo")); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResultsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResultsTests.java index 5c26fea0c06f5..5414f97e08783 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResultsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/RegressionInferenceResultsTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.ml.inference.results.RegressionInferenceResults; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.RegressionConfig; +import org.elasticsearch.xpack.core.ml.inference.trainedmodel.RegressionConfigTests; import java.util.HashMap; @@ -19,15 +20,15 @@ public class RegressionInferenceResultsTests extends AbstractWireSerializingTestCase { public static RegressionInferenceResults createRandomResults() { - return new RegressionInferenceResults(randomDouble()); + return new RegressionInferenceResults(randomDouble(), RegressionConfigTests.randomRegressionConfig()); } public void testWriteResults() { - RegressionInferenceResults result = new RegressionInferenceResults(0.3); + RegressionInferenceResults result = new RegressionInferenceResults(0.3, RegressionConfig.EMPTY_PARAMS); IngestDocument document = new IngestDocument(new HashMap<>(), new HashMap<>()); - result.writeResult(document, "result_field", new RegressionConfig()); + result.writeResult(document, "result_field"); - assertThat(document.getFieldValue("result_field", Double.class), equalTo(0.3)); + assertThat(document.getFieldValue("result_field.predicted_value", Double.class), equalTo(0.3)); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfigTests.java index aff29d9bf8b74..bc5a9fd851bb1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ClassificationConfigTests.java @@ -19,17 +19,20 @@ public class ClassificationConfigTests extends AbstractWireSerializingTestCase configMap = new HashMap<>(); configMap.put(ClassificationConfig.NUM_TOP_CLASSES.getPreferredName(), 3); - configMap.put(ClassificationConfig.TOP_CLASSES_RESULT_FIELD.getPreferredName(), "foo"); + configMap.put(ClassificationConfig.RESULTS_FIELD.getPreferredName(), "foo"); + configMap.put(ClassificationConfig.TOP_CLASSES_RESULTS_FIELD.getPreferredName(), "bar"); assertThat(ClassificationConfig.fromMap(configMap), equalTo(expected)); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/RegressionConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/RegressionConfigTests.java index bdb0e6d03201f..addf24ed8ae61 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/RegressionConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/RegressionConfigTests.java @@ -10,18 +10,23 @@ import org.elasticsearch.test.AbstractWireSerializingTestCase; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import static org.hamcrest.Matchers.equalTo; public class RegressionConfigTests extends AbstractWireSerializingTestCase { public static RegressionConfig randomRegressionConfig() { - return new RegressionConfig(); + return new RegressionConfig(randomBoolean() ? null : randomAlphaOfLength(10)); } public void testFromMap() { - RegressionConfig expected = new RegressionConfig(); - assertThat(RegressionConfig.fromMap(Collections.emptyMap()), equalTo(expected)); + RegressionConfig expected = new RegressionConfig("foo"); + Map config = new HashMap<>(){{ + put(RegressionConfig.RESULTS_FIELD.getPreferredName(), "foo"); + }}; + assertThat(RegressionConfig.fromMap(config), equalTo(expected)); } public void testFromMapWithUnknownField() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/EnsembleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/EnsembleTests.java index c38591ab6cfc5..13a401117b479 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/EnsembleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/EnsembleTests.java @@ -413,12 +413,12 @@ public void testRegressionInference() { List featureVector = Arrays.asList(0.4, 0.0); Map featureMap = zipObjMap(featureNames, featureVector); assertThat(0.9, - closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); featureVector = Arrays.asList(2.0, 0.7); featureMap = zipObjMap(featureNames, featureVector); assertThat(0.5, - closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); // Test with NO aggregator supplied, verifies default behavior of non-weighted sum ensemble = Ensemble.builder() @@ -430,19 +430,19 @@ public void testRegressionInference() { featureVector = Arrays.asList(0.4, 0.0); featureMap = zipObjMap(featureNames, featureVector); assertThat(1.8, - closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); featureVector = Arrays.asList(2.0, 0.7); featureMap = zipObjMap(featureNames, featureVector); assertThat(1.0, - closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); featureMap = new HashMap<>(2) {{ put("foo", 0.3); put("bar", null); }}; assertThat(1.8, - closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); } public void testOperationsEstimations() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/TreeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/TreeTests.java index 7f5158706941f..9d682896569bb 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/TreeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/TreeTests.java @@ -118,7 +118,7 @@ public void testInferWithStump() { List featureVector = Arrays.asList(0.6, 0.0); Map featureMap = zipObjMap(featureNames, featureVector); // does not really matter as this is a stump assertThat(42.0, - closeTo(((SingleValueInferenceResults)tree.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)tree.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); } public void testInfer() { @@ -138,27 +138,27 @@ public void testInfer() { List featureVector = Arrays.asList(0.6, 0.0); Map featureMap = zipObjMap(featureNames, featureVector); assertThat(0.3, - closeTo(((SingleValueInferenceResults)tree.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)tree.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); // This should hit the left child of the left child of the root node // i.e. it takes the path left, left featureVector = Arrays.asList(0.3, 0.7); featureMap = zipObjMap(featureNames, featureVector); assertThat(0.1, - closeTo(((SingleValueInferenceResults)tree.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)tree.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); // This should hit the right child of the left child of the root node // i.e. it takes the path left, right featureVector = Arrays.asList(0.3, 0.9); featureMap = zipObjMap(featureNames, featureVector); assertThat(0.2, - closeTo(((SingleValueInferenceResults)tree.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)tree.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); // This should still work if the internal values are strings List featureVectorStrings = Arrays.asList("0.3", "0.9"); featureMap = zipObjMap(featureNames, featureVectorStrings); assertThat(0.2, - closeTo(((SingleValueInferenceResults)tree.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)tree.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); // This should handle missing values and take the default_left path featureMap = new HashMap<>(2) {{ @@ -166,7 +166,7 @@ public void testInfer() { put("bar", null); }}; assertThat(0.1, - closeTo(((SingleValueInferenceResults)tree.infer(featureMap, new RegressionConfig())).value(), 0.00001)); + closeTo(((SingleValueInferenceResults)tree.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); } public void testTreeClassificationProbability() { diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java index 315010282a4f4..05b425363c5d3 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java @@ -127,7 +127,8 @@ public void testPipelineCreationAndDeletion() throws Exception { .size(0) .trackTotalHits(true) .query(QueryBuilders.boolQuery() - .filter(QueryBuilders.existsQuery("regression_value"))))).get().getHits().getTotalHits().value, + .filter( + QueryBuilders.existsQuery("ml.inference.regression.predicted_value"))))).get().getHits().getTotalHits().value, equalTo(20L)); assertThat(client().search(new SearchRequest().indices("index_for_inference_test") @@ -135,7 +136,12 @@ public void testPipelineCreationAndDeletion() throws Exception { .size(0) .trackTotalHits(true) .query(QueryBuilders.boolQuery() - .filter(QueryBuilders.existsQuery("result_class"))))).get().getHits().getTotalHits().value, + .filter( + QueryBuilders.existsQuery("ml.inference.classification.predicted_value"))))) + .get() + .getHits() + .getTotalHits() + .value, equalTo(20L)); } @@ -146,9 +152,9 @@ public void testSimulate() { " \"processors\": [\n" + " {\n" + " \"inference\": {\n" + - " \"target_field\": \"result_class\",\n" + + " \"target_field\": \"ml.classification\",\n" + " \"inference_config\": {\"classification\": " + - " {\"num_top_classes\":2, \"top_classes_result_field\": \"result_class_prob\"}},\n" + + " {\"num_top_classes\":2, \"top_classes_results_field\": \"result_class_prob\"}},\n" + " \"model_id\": \"test_classification\",\n" + " \"field_mappings\": {\n" + " \"col1\": \"col1\",\n" + @@ -160,7 +166,7 @@ public void testSimulate() { " },\n" + " {\n" + " \"inference\": {\n" + - " \"target_field\": \"regression_value\",\n" + + " \"target_field\": \"ml.regression\",\n" + " \"model_id\": \"test_regression\",\n" + " \"inference_config\": {\"regression\":{}},\n" + " \"field_mappings\": {\n" + @@ -186,16 +192,17 @@ public void testSimulate() { .prepareSimulatePipeline(new BytesArray(source.getBytes(StandardCharsets.UTF_8)), XContentType.JSON).get(); SimulateDocumentBaseResult baseResult = (SimulateDocumentBaseResult)response.getResults().get(0); - assertThat(baseResult.getIngestDocument().getFieldValue("regression_value", Double.class), equalTo(1.0)); - assertThat(baseResult.getIngestDocument().getFieldValue("result_class", String.class), equalTo("second")); - assertThat(baseResult.getIngestDocument().getFieldValue("result_class_prob", List.class).size(), equalTo(2)); + assertThat(baseResult.getIngestDocument().getFieldValue("ml.regression.predicted_value", Double.class), equalTo(1.0)); + assertThat(baseResult.getIngestDocument().getFieldValue("ml.classification.predicted_value", String.class), + equalTo("second")); + assertThat(baseResult.getIngestDocument().getFieldValue("ml.classification.result_class_prob", List.class).size(), + equalTo(2)); String sourceWithMissingModel = "{\n" + " \"pipeline\": {\n" + " \"processors\": [\n" + " {\n" + " \"inference\": {\n" + - " \"target_field\": \"result_class\",\n" + " \"model_id\": \"test_classification_missing\",\n" + " \"inference_config\": {\"classification\":{}},\n" + " \"field_mappings\": {\n" + @@ -526,8 +533,8 @@ private static String modelDocString(String compressedDefinition, String modelId " \"processors\": [\n" + " {\n" + " \"inference\": {\n" + - " \"target_field\": \"result_class\",\n" + " \"model_id\": \"test_classification\",\n" + + " \"tag\": \"classification\",\n" + " \"inference_config\": {\"classification\": {}},\n" + " \"field_mappings\": {\n" + " \"col1\": \"col1\",\n" + @@ -542,8 +549,8 @@ private static String modelDocString(String compressedDefinition, String modelId " \"processors\": [\n" + " {\n" + " \"inference\": {\n" + - " \"target_field\": \"regression_value\",\n" + " \"model_id\": \"test_regression\",\n" + + " \"tag\": \"regression\",\n" + " \"inference_config\": {\"regression\": {}},\n" + " \"field_mappings\": {\n" + " \"col1\": \"col1\",\n" + diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java index 0450cd4aa966b..805123cf53cc2 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java @@ -59,17 +59,14 @@ public class InferenceProcessor extends AbstractProcessor { public static final String INFERENCE_CONFIG = "inference_config"; public static final String TARGET_FIELD = "target_field"; public static final String FIELD_MAPPINGS = "field_mappings"; - public static final String MODEL_INFO_FIELD = "model_info_field"; - public static final String INCLUDE_MODEL_METADATA = "include_model_metadata"; + private static final String DEFAULT_TARGET_FIELD = "ml.inference"; private final Client client; private final String modelId; private final String targetField; - private final String modelInfoField; private final InferenceConfig inferenceConfig; private final Map fieldMapping; - private final boolean includeModelMetadata; private final InferenceAuditor auditor; private volatile boolean previouslyLicensed; private final AtomicBoolean shouldAudit = new AtomicBoolean(true); @@ -80,15 +77,11 @@ public InferenceProcessor(Client client, String targetField, String modelId, InferenceConfig inferenceConfig, - Map fieldMapping, - String modelInfoField, - boolean includeModelMetadata) { + Map fieldMapping) { super(tag); this.client = ExceptionsHelper.requireNonNull(client, "client"); this.targetField = ExceptionsHelper.requireNonNull(targetField, TARGET_FIELD); this.auditor = ExceptionsHelper.requireNonNull(auditor, "auditor"); - this.modelInfoField = ExceptionsHelper.requireNonNull(modelInfoField, MODEL_INFO_FIELD); - this.includeModelMetadata = includeModelMetadata; this.modelId = ExceptionsHelper.requireNonNull(modelId, MODEL_ID); this.inferenceConfig = ExceptionsHelper.requireNonNull(inferenceConfig, INFERENCE_CONFIG); this.fieldMapping = ExceptionsHelper.requireNonNull(fieldMapping, FIELD_MAPPINGS); @@ -153,10 +146,8 @@ void mutateDocument(InternalInferModelAction.Response response, IngestDocument i if (response.getInferenceResults().isEmpty()) { throw new ElasticsearchStatusException("Unexpected empty inference response", RestStatus.INTERNAL_SERVER_ERROR); } - response.getInferenceResults().get(0).writeResult(ingestDocument, this.targetField, inferenceConfig); - if (includeModelMetadata) { - ingestDocument.setFieldValue(modelInfoField + "." + MODEL_ID, modelId); - } + response.getInferenceResults().get(0).writeResult(ingestDocument, this.targetField); + ingestDocument.setFieldValue(targetField + "." + MODEL_ID, modelId); } @Override @@ -237,26 +228,20 @@ public InferenceProcessor create(Map processorFactori maxIngestProcessors); } - boolean includeModelMetadata = ConfigurationUtils.readBooleanProperty(TYPE, tag, config, INCLUDE_MODEL_METADATA, true); String modelId = ConfigurationUtils.readStringProperty(TYPE, tag, config, MODEL_ID); - String targetField = ConfigurationUtils.readStringProperty(TYPE, tag, config, TARGET_FIELD); + String defaultTargetField = tag == null ? DEFAULT_TARGET_FIELD : DEFAULT_TARGET_FIELD + "." + tag; + // If multiple inference processors are in the same pipeline, it is wise to tag them + // The tag will keep default value entries from stepping on each other + String targetField = ConfigurationUtils.readStringProperty(TYPE, tag, config, TARGET_FIELD, defaultTargetField); Map fieldMapping = ConfigurationUtils.readOptionalMap(TYPE, tag, config, FIELD_MAPPINGS); InferenceConfig inferenceConfig = inferenceConfigFromMap(ConfigurationUtils.readMap(TYPE, tag, config, INFERENCE_CONFIG)); - String modelInfoField = ConfigurationUtils.readStringProperty(TYPE, tag, config, MODEL_INFO_FIELD, "ml"); - // If multiple inference processors are in the same pipeline, it is wise to tag them - // The tag will keep metadata entries from stepping on each other - if (tag != null) { - modelInfoField += "." + tag; - } return new InferenceProcessor(client, auditor, tag, targetField, modelId, inferenceConfig, - fieldMapping, - modelInfoField, - includeModelMetadata); + fieldMapping); } // Package private for testing @@ -285,7 +270,7 @@ InferenceConfig inferenceConfigFromMap(Map inferenceConfig) { checkSupportedVersion(ClassificationConfig.EMPTY_PARAMS); return ClassificationConfig.fromMap(valueMap); } else if (inferenceConfig.containsKey(RegressionConfig.NAME)) { - checkSupportedVersion(new RegressionConfig()); + checkSupportedVersion(RegressionConfig.EMPTY_PARAMS); return RegressionConfig.fromMap(valueMap); } else { throw ExceptionsHelper.badRequestException("unrecognized inference configuration type {}. Supported types {}", diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/license/MachineLearningLicensingTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/license/MachineLearningLicensingTests.java index 6c2fdee78a917..760a98a3316f1 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/license/MachineLearningLicensingTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/license/MachineLearningLicensingTests.java @@ -619,7 +619,7 @@ public void testMachineLearningInferModelRestricted() throws Exception { client().execute(InternalInferModelAction.INSTANCE, new InternalInferModelAction.Request( modelId, Collections.singletonList(Collections.emptyMap()), - new RegressionConfig(), + RegressionConfig.EMPTY_PARAMS, false ), inferModelSuccess); InternalInferModelAction.Response response = inferModelSuccess.actionGet(); @@ -636,7 +636,7 @@ public void testMachineLearningInferModelRestricted() throws Exception { client().execute(InternalInferModelAction.INSTANCE, new InternalInferModelAction.Request( modelId, Collections.singletonList(Collections.emptyMap()), - new RegressionConfig(), + RegressionConfig.EMPTY_PARAMS, false )).actionGet(); }); @@ -649,7 +649,7 @@ public void testMachineLearningInferModelRestricted() throws Exception { client().execute(InternalInferModelAction.INSTANCE, new InternalInferModelAction.Request( modelId, Collections.singletonList(Collections.emptyMap()), - new RegressionConfig(), + RegressionConfig.EMPTY_PARAMS, true ), inferModelSuccess); response = inferModelSuccess.actionGet(); @@ -665,7 +665,7 @@ public void testMachineLearningInferModelRestricted() throws Exception { client().execute(InternalInferModelAction.INSTANCE, new InternalInferModelAction.Request( modelId, Collections.singletonList(Collections.emptyMap()), - new RegressionConfig(), + RegressionConfig.EMPTY_PARAMS, false ), listener); assertThat(listener.actionGet().getInferenceResults(), is(not(empty()))); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java index b9132d5e2534f..55720f73ccb25 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java @@ -45,43 +45,42 @@ public void setUpVariables() { } public void testMutateDocumentWithClassification() { - String targetField = "classification_value"; + String targetField = "ml.my_processor"; InferenceProcessor inferenceProcessor = new InferenceProcessor(client, auditor, "my_processor", targetField, "classification_model", ClassificationConfig.EMPTY_PARAMS, - Collections.emptyMap(), - "ml.my_processor", - true); + Collections.emptyMap()); Map source = new HashMap<>(); Map ingestMetadata = new HashMap<>(); IngestDocument document = new IngestDocument(source, ingestMetadata); InternalInferModelAction.Response response = new InternalInferModelAction.Response( - Collections.singletonList(new ClassificationInferenceResults(1.0, "foo", null)), + Collections.singletonList(new ClassificationInferenceResults(1.0, + "foo", + null, + ClassificationConfig.EMPTY_PARAMS)), true); inferenceProcessor.mutateDocument(response, document); - assertThat(document.getFieldValue(targetField, String.class), equalTo("foo")); - assertThat(document.getFieldValue("ml", Map.class), - equalTo(Collections.singletonMap("my_processor", Collections.singletonMap("model_id", "classification_model")))); + assertThat(document.getFieldValue(targetField + "." + ClassificationConfig.EMPTY_PARAMS.getResultsField(), String.class), + equalTo("foo")); + assertThat(document.getFieldValue("ml.my_processor.model_id", String.class), equalTo("classification_model")); } @SuppressWarnings("unchecked") public void testMutateDocumentClassificationTopNClasses() { - String targetField = "classification_value_probabilities"; + ClassificationConfig classificationConfig = new ClassificationConfig(2, null, null); InferenceProcessor inferenceProcessor = new InferenceProcessor(client, auditor, "my_processor", - targetField, - "classification_model", - new ClassificationConfig(2, null), - Collections.emptyMap(), "ml.my_processor", - true); + "classification_model", + classificationConfig, + Collections.emptyMap()); Map source = new HashMap<>(); Map ingestMetadata = new HashMap<>(); @@ -92,29 +91,26 @@ public void testMutateDocumentClassificationTopNClasses() { classes.add(new ClassificationInferenceResults.TopClassEntry("bar", 0.4)); InternalInferModelAction.Response response = new InternalInferModelAction.Response( - Collections.singletonList(new ClassificationInferenceResults(1.0, "foo", classes)), + Collections.singletonList(new ClassificationInferenceResults(1.0, "foo", classes, classificationConfig)), true); inferenceProcessor.mutateDocument(response, document); - assertThat((List>)document.getFieldValue(ClassificationConfig.DEFAULT_TOP_CLASSES_RESULT_FIELD, List.class), + assertThat((List>)document.getFieldValue("ml.my_processor.top_classes", List.class), contains(classes.stream().map(ClassificationInferenceResults.TopClassEntry::asValueMap).toArray(Map[]::new))); - assertThat(document.getFieldValue("ml", Map.class), - equalTo(Collections.singletonMap("my_processor", Collections.singletonMap("model_id", "classification_model")))); - assertThat(document.getFieldValue(targetField, String.class), equalTo("foo")); + assertThat(document.getFieldValue("ml.my_processor.model_id", String.class), equalTo("classification_model")); + assertThat(document.getFieldValue("ml.my_processor.predicted_value", String.class), equalTo("foo")); } @SuppressWarnings("unchecked") public void testMutateDocumentClassificationTopNClassesWithSpecificField() { - String targetField = "classification_value_probabilities"; + ClassificationConfig classificationConfig = new ClassificationConfig(2, "result", "tops"); InferenceProcessor inferenceProcessor = new InferenceProcessor(client, auditor, "my_processor", - targetField, - "classification_model", - new ClassificationConfig(2, "my_top_classes"), - Collections.emptyMap(), "ml.my_processor", - true); + "classification_model", + classificationConfig, + Collections.emptyMap()); Map source = new HashMap<>(); Map ingestMetadata = new HashMap<>(); @@ -125,98 +121,36 @@ public void testMutateDocumentClassificationTopNClassesWithSpecificField() { classes.add(new ClassificationInferenceResults.TopClassEntry("bar", 0.4)); InternalInferModelAction.Response response = new InternalInferModelAction.Response( - Collections.singletonList(new ClassificationInferenceResults(1.0, "foo", classes)), + Collections.singletonList(new ClassificationInferenceResults(1.0, "foo", classes, classificationConfig)), true); inferenceProcessor.mutateDocument(response, document); - assertThat((List>)document.getFieldValue("my_top_classes", List.class), + assertThat((List>)document.getFieldValue("ml.my_processor.tops", List.class), contains(classes.stream().map(ClassificationInferenceResults.TopClassEntry::asValueMap).toArray(Map[]::new))); - assertThat(document.getFieldValue("ml", Map.class), - equalTo(Collections.singletonMap("my_processor", Collections.singletonMap("model_id", "classification_model")))); - assertThat(document.getFieldValue(targetField, String.class), equalTo("foo")); + assertThat(document.getFieldValue("ml.my_processor.model_id", String.class), equalTo("classification_model")); + assertThat(document.getFieldValue("ml.my_processor.result", String.class), equalTo("foo")); } public void testMutateDocumentRegression() { - String targetField = "regression_value"; + RegressionConfig regressionConfig = new RegressionConfig("foo"); InferenceProcessor inferenceProcessor = new InferenceProcessor(client, auditor, "my_processor", - targetField, - "regression_model", - new RegressionConfig(), - Collections.emptyMap(), "ml.my_processor", - true); - - Map source = new HashMap<>(); - Map ingestMetadata = new HashMap<>(); - IngestDocument document = new IngestDocument(source, ingestMetadata); - - InternalInferModelAction.Response response = new InternalInferModelAction.Response( - Collections.singletonList(new RegressionInferenceResults(0.7)), true); - inferenceProcessor.mutateDocument(response, document); - - assertThat(document.getFieldValue(targetField, Double.class), equalTo(0.7)); - assertThat(document.getFieldValue("ml", Map.class), - equalTo(Collections.singletonMap("my_processor", Collections.singletonMap("model_id", "regression_model")))); - } - - public void testMutateDocumentNoModelMetaData() { - String targetField = "regression_value"; - InferenceProcessor inferenceProcessor = new InferenceProcessor(client, - auditor, - "my_processor", - targetField, "regression_model", - new RegressionConfig(), - Collections.emptyMap(), - "ml.my_processor", - false); + regressionConfig, + Collections.emptyMap()); Map source = new HashMap<>(); Map ingestMetadata = new HashMap<>(); IngestDocument document = new IngestDocument(source, ingestMetadata); InternalInferModelAction.Response response = new InternalInferModelAction.Response( - Collections.singletonList(new RegressionInferenceResults(0.7)), true); + Collections.singletonList(new RegressionInferenceResults(0.7, regressionConfig)), true); inferenceProcessor.mutateDocument(response, document); - assertThat(document.getFieldValue(targetField, Double.class), equalTo(0.7)); - assertThat(document.hasField("ml"), is(false)); - } - - public void testMutateDocumentModelMetaDataExistingField() { - String targetField = "regression_value"; - InferenceProcessor inferenceProcessor = new InferenceProcessor(client, - auditor, - "my_processor", - targetField, - "regression_model", - new RegressionConfig(), - Collections.emptyMap(), - "ml.my_processor", - true); - - //cannot use singleton map as attempting to mutate later - Map ml = new HashMap<>(){{ - put("regression_prediction", 0.55); - }}; - Map source = new HashMap<>(){{ - put("ml", ml); - }}; - Map ingestMetadata = new HashMap<>(); - IngestDocument document = new IngestDocument(source, ingestMetadata); - - InternalInferModelAction.Response response = new InternalInferModelAction.Response( - Collections.singletonList(new RegressionInferenceResults(0.7)), true); - inferenceProcessor.mutateDocument(response, document); - - assertThat(document.getFieldValue(targetField, Double.class), equalTo(0.7)); - assertThat(document.getFieldValue("ml", Map.class), - equalTo(new HashMap<>(){{ - put("my_processor", Collections.singletonMap("model_id", "regression_model")); - put("regression_prediction", 0.55); - }})); + assertThat(document.getFieldValue("ml.my_processor.foo", Double.class), equalTo(0.7)); + assertThat(document.getFieldValue("ml.my_processor.model_id", String.class), equalTo("regression_model")); } public void testGenerateRequestWithEmptyMapping() { @@ -228,10 +162,8 @@ public void testGenerateRequestWithEmptyMapping() { "my_processor", "my_field", modelId, - new ClassificationConfig(topNClasses, null), - Collections.emptyMap(), - "ml.my_processor", - false); + new ClassificationConfig(topNClasses, null, null), + Collections.emptyMap()); Map source = new HashMap<>(){{ put("value1", 1); @@ -259,10 +191,8 @@ public void testGenerateWithMapping() { "my_processor", "my_field", modelId, - new ClassificationConfig(topNClasses, null), - fieldMapping, - "ml.my_processor", - false); + new ClassificationConfig(topNClasses, null, null), + fieldMapping); Map source = new HashMap<>(3){{ put("value1", 1); @@ -287,10 +217,8 @@ public void testHandleResponseLicenseChanged() { "my_processor", targetField, "regression_model", - new RegressionConfig(), - Collections.emptyMap(), - "ml.my_processor", - true); + RegressionConfig.EMPTY_PARAMS, + Collections.emptyMap()); Map source = new HashMap<>(); Map ingestMetadata = new HashMap<>(); @@ -299,7 +227,7 @@ public void testHandleResponseLicenseChanged() { assertThat(inferenceProcessor.buildRequest(document).isPreviouslyLicensed(), is(false)); InternalInferModelAction.Response response = new InternalInferModelAction.Response( - Collections.singletonList(new RegressionInferenceResults(0.7)), true); + Collections.singletonList(new RegressionInferenceResults(0.7, RegressionConfig.EMPTY_PARAMS)), true); inferenceProcessor.handleResponse(response, document, (doc, ex) -> { assertThat(doc, is(not(nullValue()))); assertThat(ex, is(nullValue())); @@ -308,7 +236,7 @@ public void testHandleResponseLicenseChanged() { assertThat(inferenceProcessor.buildRequest(document).isPreviouslyLicensed(), is(true)); response = new InternalInferModelAction.Response( - Collections.singletonList(new RegressionInferenceResults(0.7)), false); + Collections.singletonList(new RegressionInferenceResults(0.7, RegressionConfig.EMPTY_PARAMS)), false); inferenceProcessor.handleResponse(response, document, (doc, ex) -> { assertThat(doc, is(not(nullValue()))); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java index a544680f5496e..41bad95fbb3a6 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java @@ -93,7 +93,7 @@ public void testRegression() throws Exception { put("categorical", "dog"); }}; - SingleValueInferenceResults results = getSingleValue(model, fields, new RegressionConfig()); + SingleValueInferenceResults results = getSingleValue(model, fields, RegressionConfig.EMPTY_PARAMS); assertThat(results.value(), equalTo(1.3)); PlainActionFuture failedFuture = new PlainActionFuture<>(); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java index 66327d31c8178..eb1064aebb21c 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java @@ -119,12 +119,15 @@ public void testInferModels() throws Exception { }}); // Test regression - InternalInferModelAction.Request request = new InternalInferModelAction.Request(modelId1, toInfer, new RegressionConfig(), true); + InternalInferModelAction.Request request = new InternalInferModelAction.Request(modelId1, + toInfer, + RegressionConfig.EMPTY_PARAMS, + true); InternalInferModelAction.Response response = client().execute(InternalInferModelAction.INSTANCE, request).actionGet(); assertThat(response.getInferenceResults().stream().map(i -> ((SingleValueInferenceResults)i).value()).collect(Collectors.toList()), contains(1.3, 1.25)); - request = new InternalInferModelAction.Request(modelId1, toInfer2, new RegressionConfig(), true); + request = new InternalInferModelAction.Request(modelId1, toInfer2, RegressionConfig.EMPTY_PARAMS, true); response = client().execute(InternalInferModelAction.INSTANCE, request).actionGet(); assertThat(response.getInferenceResults().stream().map(i -> ((SingleValueInferenceResults)i).value()).collect(Collectors.toList()), contains(1.65, 1.55)); @@ -140,7 +143,7 @@ public void testInferModels() throws Exception { contains("not_to_be", "to_be")); // Get top classes - request = new InternalInferModelAction.Request(modelId2, toInfer, new ClassificationConfig(2, null), true); + request = new InternalInferModelAction.Request(modelId2, toInfer, new ClassificationConfig(2, null, null), true); response = client().execute(InternalInferModelAction.INSTANCE, request).actionGet(); ClassificationInferenceResults classificationInferenceResults = @@ -159,7 +162,7 @@ public void testInferModels() throws Exception { greaterThan(classificationInferenceResults.getTopClasses().get(1).getProbability())); // Test that top classes restrict the number returned - request = new InternalInferModelAction.Request(modelId2, toInfer2, new ClassificationConfig(1, null), true); + request = new InternalInferModelAction.Request(modelId2, toInfer2, new ClassificationConfig(1, null, null), true); response = client().execute(InternalInferModelAction.INSTANCE, request).actionGet(); classificationInferenceResults = (ClassificationInferenceResults)response.getInferenceResults().get(0); @@ -172,7 +175,7 @@ public void testInferMissingModel() { InternalInferModelAction.Request request = new InternalInferModelAction.Request( model, Collections.emptyList(), - new RegressionConfig(), + RegressionConfig.EMPTY_PARAMS, true); try { client().execute(InternalInferModelAction.INSTANCE, request).actionGet(); From 67d3cb78a60dd5f0b46e7b87b8140c8d1e63b80a Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Thu, 12 Dec 2019 17:27:41 +0200 Subject: [PATCH 184/686] [ML] Persist/restore state for DFA classification (#50040) This commit adds state persist/restore for data frame analytics classification jobs. --- .../ml/dataframe/analyses/Classification.java | 4 ++-- .../analyses/ClassificationTests.java | 7 +++++++ .../xpack/ml/integration/ClassificationIT.java | 7 +++++++ ...lNativeDataFrameAnalyticsIntegTestCase.java | 7 +++++++ .../xpack/ml/integration/RegressionIT.java | 18 ++++++------------ 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java index ed4cb1fe18f8e..cbd78b4f3baab 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java @@ -251,12 +251,12 @@ public boolean supportsMissingValues() { @Override public boolean persistsState() { - return false; + return true; } @Override public String getStateDocId(String jobId) { - throw new UnsupportedOperationException(); + return jobId + "_classification_state#1"; } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java index 8308ef8dad289..75a7410f181ba 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java @@ -209,4 +209,11 @@ public void testToXContent_GivenEmptyParams() throws IOException { assertThat(json, containsString("randomize_seed")); } } + + public void testGetStateDocId() { + Classification classification = createRandom(); + assertThat(classification.persistsState(), is(true)); + String randomId = randomAlphaOfLength(10); + assertThat(classification.getStateDocId(randomId), equalTo(randomId + "_classification_state#1")); + } } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java index 0e49043fcfbe5..0c486fdeee678 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java @@ -95,6 +95,7 @@ public void testSingleNumericFeatureAndMixedTrainingAndNonTrainingRows() throws assertProgress(jobId, 100, 100, 100, 100); assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); + assertModelStatePersisted(stateDocId()); assertInferenceModelPersisted(jobId); assertThatAuditMessagesMatch(jobId, "Created analytics with analysis type [classification]", @@ -135,6 +136,7 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsHundred() throws Excepti assertProgress(jobId, 100, 100, 100, 100); assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); + assertModelStatePersisted(stateDocId()); assertInferenceModelPersisted(jobId); assertThatAuditMessagesMatch(jobId, "Created analytics with analysis type [classification]", @@ -195,6 +197,7 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty( assertProgress(jobId, 100, 100, 100, 100); assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); + assertModelStatePersisted(stateDocId()); assertInferenceModelPersisted(jobId); assertThatAuditMessagesMatch(jobId, "Created analytics with analysis type [classification]", @@ -447,4 +450,8 @@ private void assertEvaluation(String dependentVariable, List dependentVar assertThat(confusionMatrixResult.getOtherActualClassCount(), equalTo(0L)); } } + + protected String stateDocId() { + return jobId + "_classification_state#1"; + } } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java index 8ff82c28b36e0..980f5f4da5ecb 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java @@ -274,4 +274,11 @@ protected static Set getTrainingRowsIds(String index) { assertThat(trainingRowsIds.isEmpty(), is(false)); return trainingRowsIds; } + + protected static void assertModelStatePersisted(String stateDocId) { + SearchResponse searchResponse = client().prepareSearch(AnomalyDetectorsIndex.jobStateIndexPattern()) + .setQuery(QueryBuilders.idsQuery().addIds(stateDocId)) + .get(); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + } } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java index 84d408daacc61..29480d711f37f 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java @@ -12,14 +12,12 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsState; import org.elasticsearch.xpack.core.ml.dataframe.analyses.BoostedTreeParams; import org.elasticsearch.xpack.core.ml.dataframe.analyses.BoostedTreeParamsTests; import org.elasticsearch.xpack.core.ml.dataframe.analyses.Regression; -import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; import org.junit.After; import java.util.Arrays; @@ -82,7 +80,7 @@ public void testSingleNumericFeatureAndMixedTrainingAndNonTrainingRows() throws assertProgress(jobId, 100, 100, 100, 100); assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); - assertModelStatePersisted(jobId); + assertModelStatePersisted(stateDocId()); assertInferenceModelPersisted(jobId); assertThatAuditMessagesMatch(jobId, "Created analytics with analysis type [regression]", @@ -119,7 +117,7 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsHundred() throws Excepti assertProgress(jobId, 100, 100, 100, 100); assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); - assertModelStatePersisted(jobId); + assertModelStatePersisted(stateDocId()); assertInferenceModelPersisted(jobId); assertThatAuditMessagesMatch(jobId, "Created analytics with analysis type [regression]", @@ -171,7 +169,7 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty() throws Exception assertProgress(jobId, 100, 100, 100, 100); assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); - assertModelStatePersisted(jobId); + assertModelStatePersisted(stateDocId()); assertInferenceModelPersisted(jobId); assertThatAuditMessagesMatch(jobId, "Created analytics with analysis type [regression]", @@ -233,7 +231,7 @@ public void testStopAndRestart() throws Exception { assertProgress(jobId, 100, 100, 100, 100); assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); - assertModelStatePersisted(jobId); + assertModelStatePersisted(stateDocId()); assertInferenceModelPersisted(jobId); } @@ -324,11 +322,7 @@ private static Map getMlResultsObjectFromDestDoc(Map Date: Thu, 12 Dec 2019 15:33:22 +0000 Subject: [PATCH 185/686] [TEST] Increase timeout for ML internal cluster cleanup (#50142) Closes #48511 --- .../xpack/ml/support/BaseMlIntegTestCase.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java index 60b524ae0a025..ef226f02cf5a1 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java @@ -313,24 +313,24 @@ public static void deleteAllJobs(Logger logger, Client client) throws Exception try { CloseJobAction.Request closeRequest = new CloseJobAction.Request(MetaData.ALL); - closeRequest.setCloseTimeout(TimeValue.timeValueSeconds(30L)); + // This usually takes a lot less than 90 seconds, but has been observed to be very slow occasionally + // in CI and a 90 second timeout will avoid the cost of investigating these intermittent failures. + // See https://github.com/elastic/elasticsearch/issues/48511 + closeRequest.setCloseTimeout(TimeValue.timeValueSeconds(90L)); logger.info("Closing jobs using [{}]", MetaData.ALL); - CloseJobAction.Response response = client.execute(CloseJobAction.INSTANCE, closeRequest) - .get(); + CloseJobAction.Response response = client.execute(CloseJobAction.INSTANCE, closeRequest).get(); assertTrue(response.isClosed()); } catch (Exception e1) { try { CloseJobAction.Request closeRequest = new CloseJobAction.Request(MetaData.ALL); closeRequest.setForce(true); closeRequest.setCloseTimeout(TimeValue.timeValueSeconds(30L)); - CloseJobAction.Response response = - client.execute(CloseJobAction.INSTANCE, closeRequest).get(); + CloseJobAction.Response response = client.execute(CloseJobAction.INSTANCE, closeRequest).get(); assertTrue(response.isClosed()); } catch (Exception e2) { logger.warn("Force-closing jobs failed.", e2); } - throw new RuntimeException("Had to resort to force-closing job, something went wrong?", - e1); + throw new RuntimeException("Had to resort to force-closing job, something went wrong?", e1); } for (final Job job : jobs.results()) { From 9e91e49f4fb0a846f5bb671755e822bc59fe87dc Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 12 Dec 2019 08:20:39 -0800 Subject: [PATCH 186/686] [DOCS] Updates transform screenshots and text (#50059) --- .../transform/ecommerce-tutorial.asciidoc | 30 ++++++++++-------- .../transform/images/dataframe-transforms.jpg | Bin 246311 -> 0 bytes .../transform/images/ecommerce-batch.jpg | Bin 126036 -> 0 bytes .../transform/images/ecommerce-continuous.jpg | Bin 198381 -> 0 bytes .../transform/images/ecommerce-pivot1.jpg | Bin 500724 -> 422956 bytes .../transform/images/ecommerce-pivot2.jpg | Bin 571857 -> 493958 bytes .../transform/images/manage-transforms.jpg | Bin 0 -> 196675 bytes .../transform/images/ml-dataframepivot.jpg | Bin 93753 -> 0 bytes .../transform/images/pivot-preview.jpg | Bin 0 -> 140881 bytes docs/reference/transform/overview.asciidoc | 2 +- 10 files changed, 17 insertions(+), 15 deletions(-) delete mode 100644 docs/reference/transform/images/dataframe-transforms.jpg delete mode 100644 docs/reference/transform/images/ecommerce-batch.jpg delete mode 100644 docs/reference/transform/images/ecommerce-continuous.jpg create mode 100644 docs/reference/transform/images/manage-transforms.jpg delete mode 100644 docs/reference/transform/images/ml-dataframepivot.jpg create mode 100644 docs/reference/transform/images/pivot-preview.jpg diff --git a/docs/reference/transform/ecommerce-tutorial.asciidoc b/docs/reference/transform/ecommerce-tutorial.asciidoc index 311dc8b777d5a..6a640f5bc37a3 100644 --- a/docs/reference/transform/ecommerce-tutorial.asciidoc +++ b/docs/reference/transform/ecommerce-tutorial.asciidoc @@ -50,7 +50,7 @@ they purchased. Or you might want to take the currencies or geographies into consideration. What are the most interesting ways you can transform and interpret this data? -Go to *Machine Learning* > *Data Frames* in {kib} and use the +Go to *Management* > *Elasticsearch* > *Transforms* in {kib} and use the wizard to create a {transform}: [role="screenshot"] @@ -63,25 +63,25 @@ Let's add some more aggregations to learn more about our customers' orders. For example, let's calculate the total sum of their purchases, the maximum number of products that they purchased in a single order, and their total number of orders. We'll accomplish this by using the -{ref}/search-aggregations-metrics-sum-aggregation.html[`sum` aggregation] on the +<> on the `taxless_total_price` field, the -{ref}/search-aggregations-metrics-max-aggregation.html[`max` aggregation] on the +<> on the `total_quantity` field, and the -{ref}/search-aggregations-metrics-cardinality-aggregation.html[`cardinality` aggregation] +<> on the `order_id` field: [role="screenshot"] image::images/ecommerce-pivot2.jpg["Adding multiple aggregations to a {transform} in {kib}"] TIP: If you're interested in a subset of the data, you can optionally include a -{ref}/search-request-body.html#request-body-search-query[query] element. In this +<> element. In this example, we've filtered the data so that we're only looking at orders with a `currency` of `EUR`. Alternatively, we could group the data by that field too. If you want to use more complex queries, you can create your {dataframe} from a {kibana-ref}/save-open-search.html[saved search]. If you prefer, you can use the -{ref}/preview-transform.html[preview {transforms} API]: +<>: [source,console] -------------------------------------------------- @@ -147,16 +147,13 @@ target index does not exist, it will be created automatically. Since this sample data index is unchanging, let's use the default behavior and just run the {transform} once. -[role="screenshot"] -image::images/ecommerce-batch.jpg["Specifying the {transform} options in {kib}"] - If you want to try it out, however, go ahead and click on *Continuous mode*. You must choose a field that the {transform} can use to check which entities have changed. In general, it's a good idea to use the ingest timestamp field. In this example, however, you can use the `order_date` field. If you prefer, you can use the -{ref}/put-transform.html[create {transforms} API]. For +<>. For example: [source,console] @@ -228,11 +225,11 @@ can stop it. You can start, stop, and manage {transforms} in {kib}: [role="screenshot"] -image::images/dataframe-transforms.jpg["Managing {transforms} in {kib}"] +image::images/manage-transforms.jpg["Managing {transforms} in {kib}"] Alternatively, you can use the -{ref}/start-transform.html[start {transforms}] and -{ref}/stop-transform.html[stop {transforms}] APIs. For +<> and +<> APIs. For example: [source,console] @@ -241,6 +238,11 @@ POST _transform/ecommerce-customer-transform/_start -------------------------------------------------- // TEST[skip:setup kibana sample data] +TIP: If you chose a batch {transform}, it is a single operation that has a +single checkpoint. You cannot restart it when it's complete. {ctransforms-cap} +differ in that they continually increment and process checkpoints as new source +data is ingested. + -- . Explore the data in your new index. @@ -255,6 +257,6 @@ image::images/ecommerce-results.jpg["Exploring the new index in {kib}"] TIP: If you do not want to keep the {transform}, you can delete it in {kib} or use the -{ref}/delete-transform.html[delete {transform} API]. When +<>. When you delete a {transform}, its destination index and {kib} index patterns remain. diff --git a/docs/reference/transform/images/dataframe-transforms.jpg b/docs/reference/transform/images/dataframe-transforms.jpg deleted file mode 100644 index 927678f894d4ba2b411560d60665b6f6e9b71570..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 246311 zcmeFZdpwlu*EoEaP!vgsVmdg4(1GNT+0{k}A>=sQmcy8soCjlO?;<&6Cx@NW6fw@q zF{harhf0!jMvNIDr_6*g%;~-D?|1M0{XU=H`#kUayzhU{!^b^q*0rv6t+noTU)Q?U zx^DbW{3&3+xrvzxuww@RTm^ptJ{9=E7zOtR082|i4FG`Mz>YXLunUAh5mjlIr4|>z@Wzex&7aW|Kk_YTVBDQpz^!H z2IQ?^Pk#_b0f1nR=bfMs01!S1;xB}U+}Vb=WjGiF3JAhKZNonQfWbE4;LrbnAN)ac z!}bbDlLNwHp1vNpK)4))PyPHac%OfP{~-fN0rD~R3h)Yodmi1k#q}VhH~hBO9}WMw z{1?stLiwM8{wgX{B5GIGg9jyKd z^X`OPwgq8j&{d9yBJ3}LFz8D=uX+cW*n=?0XJ?Ra=+!^rtVrJ*jv!td#J30hY-Rn= zzMnjUuYk4$j)Qofub0`jeH8#e;3z!A>;?#fd<2Ywkha_OpbP@ux4eEd2H{g6oPa=D zY|8-h6sQjlv)jgR%kwGH_sX`M+xYi>9_H6U81!lZn%8a9ZF+Dl0nVL}>;IHtmqNg8 ztL<^Yv3C9F75s<3AfH_dA-*@Rf_QM;UAIGmZv3cjr=&m1j8SVNJ#Om!b_>1>-u+N_wZObzcj`9yaf#8Tg@F|I&1tZk8X6!p}$8P-u23LaBnx-JK#6Dzp>j^@QSlyS^;-Y{f#|6dV2Zv@abo# zpPyb7cq~vS&??X<&?N94I4ba3piO`%&?QhM(EJzuf96Af-FskL@@>D`mV4W~{?zba zZ4G*?*k!T9Vkg9Y6oZHZq8E*cq{_Vk%(E;lJ+N-~N5{Uo{8q;slQNr+xm< zV{Jbx@V_{L_h839&;#IqkN9ssU=K)ZU-fW+<{wL)hE zc1Qp_C3Xl%?BI6*;8L?gaK|4%|6~m8*eS3}P)K<99uZNHpl&~~bBBPy&Rqh6g4+Sb zj%ctQ*d-x&;Mkct4{)zw)E8iRo1{bGsY%4vwIDp0~WbeSG15!6Bhx;i!nndoi*1;~vB(q&`kd z&&d4!NmhQr^THy0aY<=)O>JF$!>h)oc2Y-YSNEH@?*;~khDSy}jghIe=`S<0b6@8d z7~fad)|nfu&8=;{b^rqZPz${NL$m*&7ifeXJ9q68*d@HJ*N&ax+lou<5=T4cqat}IwsZW4R>p!QY2749ZkY9 zEl_W|X?P_n+i9>?USY(dBz1C3Q|F=sZRDNst5+EpjyiceWcXYy*t=9ZDiU0xByuO$ z%z%7439sVsRv_<|QMK|lGQz|nCs0T~!|}Hny}a-zqseAAJ=IA<>Swzhk`I|zIN7%U zH1E)HBU#NR-TdJ}a?AP_MUx_jtQJ|Ncp3vu%)q-V8)P?*REp1NA| zBu9)$ykp{tkhR-}SNK4OdOUV>oQv+{1NXDJ+0B@b*j?h>;glWN@0ĩkp7C3a0T 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 V>i7k~-LkIEcYkdZy2SkdCIAo;Npb)H diff --git a/docs/reference/transform/images/ecommerce-batch.jpg b/docs/reference/transform/images/ecommerce-batch.jpg deleted file mode 100644 index bed3fedd4cf01a385df2059ed2e59f1116b6119b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126036 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 z 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 diff --git a/docs/reference/transform/images/ecommerce-continuous.jpg b/docs/reference/transform/images/ecommerce-continuous.jpg deleted file mode 100644 index f144fc8cb9541bf12150044fea22f2e650302f91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 198381 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 Yf_5i={$KF){x2Tu{!0d9@Xz`G0Z3PR!2kdN diff --git a/docs/reference/transform/images/ecommerce-pivot1.jpg b/docs/reference/transform/images/ecommerce-pivot1.jpg index b55b88b8acfb0f6a3dfc032f35b065a35c30f541..d7ecaa666e89302d0f8dc85a0b719d55e4c2b6e5 100644 GIT binary patch literal 422956 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�X3X&lOtLN&DLVGFKdbMubZ~ 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

    &3`}stNQ;N<;@&avx_E6pe45T{hd{;JhasDxB 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�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 literal 500724 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

    3&SA37QqZ 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%0A$#eeo2;60Nq7+|_; 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�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%0A9~TU*&TM<4{Vbq1GR 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 DDzbr* diff --git a/docs/reference/transform/images/ecommerce-pivot2.jpg b/docs/reference/transform/images/ecommerce-pivot2.jpg index 9af5a3c46b74003ca256cde2f2487da9c13cdc10..7c27694b55592a6a29e0bd1ea5f587cd4ecfcd20 100644 GIT binary patch literal 493958 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 z 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%0DwF~;_{k`!1I&I;y5R#>+D>7 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

    Mq`s=cz4`&3;=YM5~Kn!{TS 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ⅆLQ{D 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�%~`oTU=g=lAYCOBgAgsgMT%E$ltBWLkfF`76%B)sX`76HvrXCb+GIEB%@Y5vs8t6bQ;{_utzM#;R1%XcEPeboLZqcF zAgvP^Bau|%zaKk4;uqLrLvHv%J^XwtOV&CKs05%nSX zYnK`4OAbH#8#H#>Rsx!DFwrONHXCKgL!K>(IQ{0qgm8g7EOKf1B!VOXHJMxa>Vgo5 z2%Vwgl&)H{Jaa5cSrk_gMz$-rdV+mbX_j`Pv!&QwH{>0)&p2(!8qVh4G^qX+pn2j? zh6tXJ-EBU?4w6UlVGYjkz2zkFLBg3nqTCsd^--kK)X0|lUQ(&1k;`rN|HIy!M>Umx z+oD$4i-?Mdw6OsZ5Ga*K+SmaCLKFl9gs6xRVu;cP2uUfO(nysG{G^KrAtHoG6CzzA zgr*b$X(K@h3DOft5WG*)hfotiZoz=2eaV zT@l3`t%{jw#>-1il>+@d;8AZV1B}2{RHv!TvxmA?b+(2JS4dmzY_LemyVK6?#;X06eo^ z`=P~=u{m~k*?lAlF_f}=*raW=^~%<8*Wnww#oziIYF8XWnj3;w`~r25LfZK1T5BgP zem`7MvOP+Oye!ay?>mlD_rVm|6WXH)yGRUQLj^T&AV$A^fF$_DjwhFUw|XoEXqEr! z_-ygE@?Z2E|2T8w-r4r(tq(0VPtUrZ z_-|PFe+(-BchPIjFrhzhnu&f;uzAQv8|x*E%p50dnt=y6pZ6K`j`XN!)II5R?hhBM z9Sfma`+E%<-AU?j=rvB&=li%f<>))xe4OH4c+zw!SE~z?e4;S(_7}b3B~M+iQC2Z-_A! z9#f^-JHks__T+mHzUg+%*E2>8lzD_+CT({pMgkiBz9&)gCwx_12WJlO4d;V~DV8z+F{{aceios>8qzayvY zV)7QqQFJ4D}pz|ik)@by?>(>{_WQPm)Pu~Q=wL@D7(XVay)kfY&@PZ6l_OTD7tn{Vndn70Vhdk~*S&MW;=+};=Mvl<}Q`5K= z8lO%moxn)qwcB}bUcY&lcgOuf+bzA&bH?LuJjizLM+;p}Bt#SS&z(rSTjVcv>1fB9 z3#L0d;mJ8s3*1+1e6zQK%^sbF=^|u|XZ)(OrO2vLnty2EZt+`bZNgBkE^*dx!{?QJ z8-*QTC5E+w*&UxTe~m~KsMPE;D6`gC_S}ch-y&`cp3rF3c)F;bPiq5^TrYJ!LN8ld5|jCIq{YTuxu>Hbu&JCQXJHZB}5z zlL*y1@4<$qj+S~+rXXrOKe?5lfbbYvgYLmX1jzS(n}#SRrN)GXu2}DW=-ZmYDDET+ zEBszvUiyjJ@VSI-1|ea>=&GDJH#(`sEQR zwfVM+p@Ong?lAA|3y-Y|k4mPuyXBI+)3*)8=V>47%xo7vf3*^7|5@oVfAjxq@&R*c z1=ulYv_sYb&YzjJGuVOc^%0_KE+^h^9yDR#!v;Xp2H3r)OJS2 z=xn`}XQ;haWO|IDpYGnHc9W#7uJ<2ChDJ8bPP2&MP>fMnw9QpcK$tMM9V8z;B}ydG zH2^Z6F~SnP5>OV(c=+Kmk0t<5#-gQpGG$CuN4GN!;ynOpJ zzyqGqX49sk43(g246c0$QC#nA7{otBxKcHHRj{fx>lU$Z;fIc{ZF{J%`qdChAQCtv=&?9w6=d6bChxF-C#<_lrA=q<=SJ;q)Iei6y4*woppOjJNNg<809H6E zHpUpIcARd#pxDM_P;#Esh4?$%;STrlgo&)wJGB18eJ>kiA8Q1neA2w~nwcld%+&N_ zkxN=oI~g9tu`29TR*H>6S^AI}8pR{I&(ge|GA*g2j1GI?ha6H2!#6vEqgw=!gN4}0 z%Q`GN7HER+&G5>|LM|+Uqipk=1SNoEVBHit;Oq++w@bpU8CC}4U=HTrR)3xT3lR4b z7NwCnMv9K6%xCq)g@~SV7Ju_8=uSM{;vW5NPTZSzJ6qdwW(4uww8_$U__xsKxtFUZ zN1H;#sM~bd6xLb18}}7=Q2NKu4Y#0xz9%0ojOXGAB#AG`tBscsR95CW57*I;cLzPi zMhc0^&6Lev7(ImpW9N6ncAT9+oh8-@wZCpRw%J!xH{h*PXKT+UYrB@H<^?Q@w~2?u zaxcb4`Tcb#vX2KIrB27dD^fdI@OG9p!ydfpSvVG_x~CO3+~F;KEjx{GwuA=36F4(IhoGgnkfchosOqeqNnUwG-lA=h!`qQ}|c5 z$W<-yQD<(UGsNw49a>8of{BXGibGbUeT# z7=Z2)|ExF&9Kie)w{f0OMQd4l2&sbb<;E#ngYmmTWd(%}O(W1Kx6zCOSTg;N)&uzB z8C7U%c+(NFtEGNA(yd8IT@X3Q1$%GF4j`JxUuh@}pK&8PhPi9&{ejLjIZ%>cN4dkX zMOl80$_;h=aFmLBOl7&3kR#@BtDc7nf!PTowtov}8-AQbpzZ}axjijqknIw%zLpC6|QPSXhl%;a2|P@2&z{`ZMn2fH7sq2bB7i`BIJHhI>a+ z3^TXt?JJ**X5G8OXo$=t7{xY$PBMRn7q57qTXPy4ot>`yQ6_elV8KtQmV|>puggAz z!{n)0U9mob06Gi7hKqQnZwb{~a*F{W&Kyu1hql7E&v|I71JA&WGT1qEx~2Z_p4SZd zxq5sCC}GgW^|98u{Mlo%r8~~ZD`NZC=yoXr^>vi4l7} zX=?l%|07R2iVhrFpGSj6=_^OXV2i;m#_+bCH*lb1=!TAs8@Yjr_aAg`u?CIECdwq< zjMZiQx|>NxOOx_9+#R-T055Ru8FAu8@eSg{o1DZ35x5>qDqkevr2;KY1UIhRl&GPt31`7~D1cR_OSf z`}0ZjHs@s%&5NDLF{2kh5OXWH{X)^%nOgh`8WL~YFLWO3T~l~WsDVoMMR`cG@qsSE&)wjx{(x8G9_f&MW9y?00MB zudB<|aD9Rm?gKa}=L!QGb%kr-2(~HK9Y_}&%0A=w;O{_&KjA*pH%MQC_hV}#I#FiO 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

    gF!@x*`>{5m(tNTO&K~T}$ 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

    vgDSrPq;YFZ>Xc&mc)F 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 JX7c>M2>==RM!f(4 literal 0 HcmV?d00001 diff --git a/docs/reference/transform/overview.asciidoc b/docs/reference/transform/overview.asciidoc index 07c17f778abaf..d35f0afbd39a1 100644 --- a/docs/reference/transform/overview.asciidoc +++ b/docs/reference/transform/overview.asciidoc @@ -59,7 +59,7 @@ quantity. The result is an entity-centric index that shows the number of sold items in every product category in the last year. [role="screenshot"] -image::images/ml-dataframepivot.jpg["Example of a data frame pivot in {kib}"] +image::images/pivot-preview.jpg["Example of a {transform} pivot in {kib}"] IMPORTANT: The {transform} leaves your source index intact. It creates a new index that is dedicated to the transformed data. From 87919385cedce5b7e1c198d551447cbb6be03fef Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Thu, 12 Dec 2019 11:34:24 -0800 Subject: [PATCH 187/686] Reenable the 'continue scroll' cluster upgrade test. --- .../resources/rest-api-spec/test/upgraded_cluster/10_basic.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/10_basic.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/10_basic.yml index ce30d31a993c1..0c5deab19068d 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/10_basic.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/10_basic.yml @@ -1,8 +1,5 @@ --- "Continue scroll after upgrade": - - skip: - version: "all" - reason: "AwaitsFix https://github.com/elastic/elasticsearch/issues/46529" - do: get: index: scroll_index From 156bfdb9f74e363a7d36b5bae473d77355215493 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Thu, 12 Dec 2019 15:41:20 -0500 Subject: [PATCH 188/686] [DOCS] Document JVM node stats (#49500) * [DOCS] Document JVM node stats Documents the `jvm` parameters returned by the `_nodes/stats` API. Co-Authored-By: James Baiera --- docs/reference/cluster/nodes-stats.asciidoc | 149 +++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/docs/reference/cluster/nodes-stats.asciidoc b/docs/reference/cluster/nodes-stats.asciidoc index 145315ac02917..d607c6430d789 100644 --- a/docs/reference/cluster/nodes-stats.asciidoc +++ b/docs/reference/cluster/nodes-stats.asciidoc @@ -721,7 +721,6 @@ NOTE: For the cgroup stats to be visible, cgroups must be compiled into the kernel, the `cpu` and `cpuacct` cgroup subsystems must be configured and stats must be readable from `/sys/fs/cgroup/cpu` and `/sys/fs/cgroup/cpuacct`. - [[cluster-nodes-stats-api-response-body-process]] ===== `process` section @@ -747,6 +746,154 @@ must be readable from `/sys/fs/cgroup/cpu` and `/sys/fs/cgroup/cpuacct`. Size in bytes of virtual memory that is guaranteed to be available to the running process. +[[cluster-nodes-stats-api-response-body-jvm]] +===== `jvm` section + +`jvm.timestamp`:: +(integer) +Last time JVM statistics were refreshed. + +`jvm.uptime_in_millis`:: +(integer) +JVM uptime in milliseconds. + +`jvm.mem.heap_used_in_bytes`:: +(integer) +Memory, in bytes, currently in use by the heap. + +`jvm.mem.heap_used_percent`:: +(integer) +Percentage of memory currently in use by the heap. + +`jvm.mem.heap_committed_in_bytes`:: +(integer) +Amount of memory, in bytes, available for use by the heap. + +`jvm.mem.heap_max_in_bytes`:: +(integer) +Maximum amount of memory, in bytes, available for use by the heap. + +`jvm.mem.non_heap_used_in_bytes`:: +(integer) +Non-heap memory used, in bytes. + +`jvm.mem.non_heap_committed_in_bytes`:: +(integer) +Amount of non-heap memory available, in bytes. + +`jvm.mem.pools.young.used_in_bytes`:: +(integer) +Memory, in bytes, used by the young generation heap. + +`jvm.mem.pools.young.max_in_bytes`:: +(integer) +Maximum amount of memory, in bytes, available for use by the young generation +heap. + +`jvm.mem.pools.young.peak_used_in_bytes`:: +(integer) +Largest amount of memory, in bytes, historically used by the young generation +heap. + +`jvm.mem.pools.young.peak_max_in_bytes`:: +(integer) +Largest amount of memory, in bytes, historically used by the young generation +heap. + +`jvm.mem.pools.survivor.used_in_bytes`:: +(integer) +Memory, in bytes, used by the survivor space. + +`jvm.mem.pools.survivor.max_in_bytes`:: +(integer) +Maximum amount of memory, in bytes, available for use by the survivor space. + +`jvm.mem.pools.survivor.peak_used_in_bytes`:: +(integer) +Largest amount of memory, in bytes, historically used by the survivor space. + +`jvm.mem.pools.survivor.peak_max_in_bytes`:: +(integer) +Largest amount of memory, in bytes, historically used by the survivor space. + +`jvm.mem.pools.old.used_in_bytes`:: +(integer) +Memory, in bytes, used by the old generation heap. + +`jvm.mem.pools.old.max_in_bytes`:: +(integer) +Maximum amount of memory, in bytes, available for use by the old generation +heap. + +`jvm.mem.pools.old.peak_used_in_bytes`:: +(integer) +Largest amount of memory, in bytes, historically used by the old generation +heap. + +`jvm.mem.pools.old.peak_max_in_bytes`:: +(integer) +Highest memory limit, in bytes, historically available for use by the old +generation heap. + +`jvm.threads.count`:: +(integer) +Number of active threads in use by JVM. + +`jvm.threads.peak_count`:: +(integer) +Highest number of threads used by JVM. + +`jvm.gc.collectors.young.collection_count`:: +(integer) +Number of JVM garbage collectors that collect young generation objects. + +`jvm.gc.collectors.young.collection_time_in_millis`:: +(integer) +Total time in milliseconds spent by JVM collecting young generation objects. + +`jvm.gc.collectors.old.collection_count`:: +(integer) +Number of JVM garbage collectors that collect old generation objects. + +`jvm.gc.collectors.old.collection_time_in_millis`:: +(integer) +Total time in milliseconds spent by JVM collecting old generation objects. + +`jvm.buffer_pools.mapped.count`:: +(integer) +Number of mapped buffer pools. + +`jvm.buffer_pools.mapped.used_in_bytes`:: +(integer) +Size, in bytes, of mapped buffer pools. + +`jvm.buffer_pools.mapped.total_capacity_in_bytes`:: +(integer) +Total capacity, in bytes, of mapped buffer pools. + +`jvm.buffer_pools.direct.count`:: +(integer) +Number of direct buffer pools. + +`jvm.buffer_pools.direct.used_in_bytes`:: +(integer) +Size, in bytes, of direct buffer pools. + +`jvm.buffer_pools.direct.total_capacity_in_bytes`:: +(integer) +Total capacity, in bytes, of direct buffer pools. + +`jvm.classes.current_loaded_count`:: +(integer) +Number of buffer pool classes currently loaded by JVM. + +`jvm.classes.total_loaded_count`:: +(integer) +Total number of buffer pool classes loaded since the JVM started. + +`jvm.classes.total_unloaded_count`:: +(integer) +Total number of buffer pool classes unloaded since the JVM started. [[cluster-nodes-stats-api-response-body-ingest]] ===== `ingest` section From 244a8e71b6eb305ec2b27f29b704be4bc5c5ee68 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Thu, 12 Dec 2019 14:52:36 -0800 Subject: [PATCH 189/686] Don't pass OPENSHIFT_IP env variable when building old BWC branches (#50153) This commit tweaks the workaround introduced in #49211 to support Gradle 6.0. In the workaround, we specifically override the address the Gradle daemon binds to by passing the desired address via the OPENSHIFT_IP environment variable. This works fine for builds using Gradle 6.0, but for older Gradle versions this causes issues with inter-daemon communication, specifically when we build BWC branches not on Gradle 6.0. The fix here is to strip that environment variable out when building the target BWC branch if that branch is on an older Gradle version. This is all temporary and will be removed when this bug fix is released in Gradle 6.1. Closes #50025 --- distribution/bwc/build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/distribution/bwc/build.gradle b/distribution/bwc/build.gradle index e96ee75ae4342..dca0d18ac4aee 100644 --- a/distribution/bwc/build.gradle +++ b/distribution/bwc/build.gradle @@ -23,6 +23,7 @@ import org.elasticsearch.gradle.Version import org.elasticsearch.gradle.BwcVersions import org.elasticsearch.gradle.info.BuildParams import org.elasticsearch.gradle.info.GlobalBuildInfoPlugin +import org.gradle.util.GradleVersion import java.nio.charset.StandardCharsets @@ -161,6 +162,12 @@ bwcVersions.forPreviousUnreleased { BwcVersions.UnreleasedVersionInfo unreleased spoolOutput = true workingDir = checkoutDir doFirst { + // TODO: Remove this once we've updated to Gradle 6.1 + // Workaround for https://github.com/gradle/gradle/issues/11426 + if (GradleVersion.version(file("${ checkoutDir}/buildSrc/src/main/resources/minimumGradleVersion").text) != GradleVersion.current()) { + environment = environment.findAll { key, val -> key != 'OPENSHIFT_IP' } + } + // Execution time so that the checkouts are available List lines = file("${checkoutDir}/.ci/java-versions.properties").readLines() environment( From 33af9c58e14d44c3cb45a469c6d9c7b07de251a6 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Thu, 12 Dec 2019 19:17:59 -0500 Subject: [PATCH 190/686] Validate exporter type is HTTP for HTTP exporter (#49992) Today the HTTP exporter settings without the exporter type having been configured to HTTP. When it is time to initialize the exporter, we can blow up. Since this initialization happens on the cluster state applier thread, it is quite problematic that we do not reject settings updates where the type is not configured to HTTP, but there are HTTP exporter settings configured. This commit addresses this by validating that the exporter type is not only set, but is set to HTTP. --- .../exporter/http/HttpExporter.java | 74 +++++++++++++------ .../exporter/http/HttpExporterSslIT.java | 2 + .../exporter/http/HttpExporterTests.java | 13 ++-- 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporter.java b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporter.java index dd9041ffd070c..a73cee446c280 100644 --- a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporter.java +++ b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporter.java @@ -78,6 +78,20 @@ public class HttpExporter extends Exporter { public static final String TYPE = "http"; + private static Setting.AffixSettingDependency TYPE_DEPENDENCY = new Setting.AffixSettingDependency() { + @Override + public Setting.AffixSetting getSetting() { + return Exporter.TYPE_SETTING; + } + + @Override + public void validate(final String key, final Object value, final Object dependency) { + if (TYPE.equals(dependency) == false) { + throw new SettingsException("[" + key + "] is set but type is [" + dependency + "]"); + } + } + }; + /** * A string array representing the Elasticsearch node(s) to communicate with over HTTP(S). */ @@ -111,9 +125,6 @@ public void validate(final List hosts, final Map, Object> set } else { throw new SettingsException("host list for [" + key + "] is empty but type is [" + type + "]"); } - } else if ("http".equals(type) == false) { - // the hosts can only be non-empty if the type is "http" - throw new SettingsException("host list for [" + key + "] is set but type is [" + type + "]"); } boolean httpHostFound = false; @@ -129,7 +140,7 @@ public void validate(final List hosts, final Map, Object> set throw new SettingsException("[" + key + "] invalid host: [" + host + "]", e); } - if ("http".equals(httpHost.getSchemeName())) { + if (TYPE.equals(httpHost.getSchemeName())) { httpHostFound = true; } else { httpsHostFound = true; @@ -152,26 +163,31 @@ public Iterator> settings() { }, Property.Dynamic, - Property.NodeScope)); + Property.NodeScope), + TYPE_DEPENDENCY); /** * Master timeout associated with bulk requests. */ public static final Setting.AffixSetting BULK_TIMEOUT_SETTING = Setting.affixKeySetting("xpack.monitoring.exporters.","bulk.timeout", - (key) -> Setting.timeSetting(key, TimeValue.MINUS_ONE, Property.Dynamic, Property.NodeScope)); + (key) -> Setting.timeSetting(key, TimeValue.MINUS_ONE, Property.Dynamic, Property.NodeScope), TYPE_DEPENDENCY); /** * Timeout used for initiating a connection. */ public static final Setting.AffixSetting CONNECTION_TIMEOUT_SETTING = - Setting.affixKeySetting("xpack.monitoring.exporters.","connection.timeout", - (key) -> Setting.timeSetting(key, TimeValue.timeValueSeconds(6), Property.Dynamic, Property.NodeScope)); + Setting.affixKeySetting( + "xpack.monitoring.exporters.", + "connection.timeout", + (key) -> Setting.timeSetting(key, TimeValue.timeValueSeconds(6), Property.Dynamic, Property.NodeScope), TYPE_DEPENDENCY); /** * Timeout used for reading from the connection. */ public static final Setting.AffixSetting CONNECTION_READ_TIMEOUT_SETTING = - Setting.affixKeySetting("xpack.monitoring.exporters.","connection.read_timeout", - (key) -> Setting.timeSetting(key, TimeValue.timeValueSeconds(60), Property.Dynamic, Property.NodeScope)); + Setting.affixKeySetting( + "xpack.monitoring.exporters.", + "connection.read_timeout", + (key) -> Setting.timeSetting(key, TimeValue.timeValueSeconds(60), Property.Dynamic, Property.NodeScope), TYPE_DEPENDENCY); /** * Username for basic auth. */ @@ -181,12 +197,12 @@ public Iterator> settings() { key, new Setting.Validator() { @Override - public void validate(String password) { + public void validate(final String password) { // no username validation that is independent of other settings } @Override - public void validate(String username, Map, Object> settings) { + public void validate(final String username, final Map, Object> settings) { final String namespace = HttpExporter.AUTH_USERNAME_SETTING.getNamespace( HttpExporter.AUTH_USERNAME_SETTING.getConcreteSetting(key)); @@ -201,6 +217,11 @@ public void validate(String username, Map, Object> settings) { "but [" + AUTH_PASSWORD_SETTING.getConcreteSettingForNamespace(namespace).getKey() + "] is " + "missing"); } + final String type = + (String) settings.get(Exporter.TYPE_SETTING.getConcreteSettingForNamespace(namespace)); + if ("http".equals(type) == false) { + throw new SettingsException("username for [" + key + "] is set but type is [" + type + "]"); + } } } @@ -209,7 +230,9 @@ public Iterator> settings() { final String namespace = HttpExporter.AUTH_USERNAME_SETTING.getNamespace( HttpExporter.AUTH_USERNAME_SETTING.getConcreteSetting(key)); + final List> settings = List.of( + Exporter.TYPE_SETTING.getConcreteSettingForNamespace(namespace), HttpExporter.AUTH_PASSWORD_SETTING.getConcreteSettingForNamespace(namespace)); return settings.iterator(); } @@ -217,7 +240,8 @@ public Iterator> settings() { }, Property.Dynamic, Property.NodeScope, - Property.Filtered)); + Property.Filtered), + TYPE_DEPENDENCY); /** * Password for basic auth. */ @@ -261,15 +285,19 @@ public Iterator> settings() { }, Property.Dynamic, Property.NodeScope, - Property.Filtered)); + Property.Filtered), + TYPE_DEPENDENCY); /** * The SSL settings. * * @see SSLService */ public static final Setting.AffixSetting SSL_SETTING = - Setting.affixKeySetting("xpack.monitoring.exporters.","ssl", - (key) -> Setting.groupSetting(key + ".", Property.Dynamic, Property.NodeScope, Property.Filtered)); + Setting.affixKeySetting( + "xpack.monitoring.exporters.", + "ssl", + (key) -> Setting.groupSetting(key + ".", Property.Dynamic, Property.NodeScope, Property.Filtered), + TYPE_DEPENDENCY); /** * Proxy setting to allow users to send requests to a remote cluster that requires a proxy base path. */ @@ -288,13 +316,14 @@ public Iterator> settings() { } }, Property.Dynamic, - Property.NodeScope)); + Property.NodeScope), + TYPE_DEPENDENCY); /** * A boolean setting to enable or disable sniffing for extra connections. */ public static final Setting.AffixSetting SNIFF_ENABLED_SETTING = Setting.affixKeySetting("xpack.monitoring.exporters.","sniff.enabled", - (key) -> Setting.boolSetting(key, false, Property.Dynamic, Property.NodeScope)); + (key) -> Setting.boolSetting(key, false, Property.Dynamic, Property.NodeScope), TYPE_DEPENDENCY); /** * A parent setting to header key/value pairs, whose names are user defined. */ @@ -316,7 +345,8 @@ public Iterator> settings() { } }, Property.Dynamic, - Property.NodeScope)); + Property.NodeScope), + TYPE_DEPENDENCY); /** * Blacklist of headers that the user is not allowed to set. *

    @@ -328,19 +358,19 @@ public Iterator> settings() { */ public static final Setting.AffixSetting TEMPLATE_CHECK_TIMEOUT_SETTING = Setting.affixKeySetting("xpack.monitoring.exporters.","index.template.master_timeout", - (key) -> Setting.timeSetting(key, TimeValue.MINUS_ONE, Property.Dynamic, Property.NodeScope)); + (key) -> Setting.timeSetting(key, TimeValue.MINUS_ONE, Property.Dynamic, Property.NodeScope), TYPE_DEPENDENCY); /** * A boolean setting to enable or disable whether to create placeholders for the old templates. */ public static final Setting.AffixSetting TEMPLATE_CREATE_LEGACY_VERSIONS_SETTING = Setting.affixKeySetting("xpack.monitoring.exporters.","index.template.create_legacy_templates", - (key) -> Setting.boolSetting(key, true, Property.Dynamic, Property.NodeScope)); + (key) -> Setting.boolSetting(key, true, Property.Dynamic, Property.NodeScope), TYPE_DEPENDENCY); /** * ES level timeout used when checking and writing pipelines (used to speed up tests) */ public static final Setting.AffixSetting PIPELINE_CHECK_TIMEOUT_SETTING = Setting.affixKeySetting("xpack.monitoring.exporters.","index.pipeline.master_timeout", - (key) -> Setting.timeSetting(key, TimeValue.MINUS_ONE, Property.Dynamic, Property.NodeScope)); + (key) -> Setting.timeSetting(key, TimeValue.MINUS_ONE, Property.Dynamic, Property.NodeScope), TYPE_DEPENDENCY); /** * Minimum supported version of the remote monitoring cluster (same major). diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterSslIT.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterSslIT.java index 3aa07a58a7666..fb0da753be3b5 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterSslIT.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterSslIT.java @@ -177,6 +177,8 @@ private Exporter getExporter(String name) { private ActionFuture setVerificationMode(String name, VerificationMode mode) { final ClusterUpdateSettingsRequest updateSettings = new ClusterUpdateSettingsRequest(); final Settings settings = Settings.builder() + .put("xpack.monitoring.exporters." + name + ".type", HttpExporter.TYPE) + .put("xpack.monitoring.exporters." + name + ".host", "https://" + webServer.getHostName() + ":" + webServer.getPort()) .put("xpack.monitoring.exporters." + name + ".ssl.verification_mode", mode.name()) .build(); updateSettings.transientSettings(settings); diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterTests.java index bfa1e16d860b0..a50839ffbadc2 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterTests.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterTests.java @@ -17,6 +17,7 @@ import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsException; import org.elasticsearch.common.unit.TimeValue; @@ -38,6 +39,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -133,14 +135,9 @@ public void testHostListIsRejectedIfTypeIsNotHttp() { final Settings.Builder builder = Settings.builder().put(prefix + ".type", "local"); builder.putList(prefix + ".host", List.of("https://example.com:443")); final Settings settings = builder.build(); - final IllegalArgumentException e = expectThrows( - IllegalArgumentException.class, - () -> HttpExporter.HOST_SETTING.getConcreteSetting(prefix + ".host").get(settings)); - assertThat( - e, - hasToString(containsString("Failed to parse value [[\"https://example.com:443\"]] for setting [" + prefix + ".host]"))); - assertThat(e.getCause(), instanceOf(SettingsException.class)); - assertThat(e.getCause(), hasToString(containsString("host list for [" + prefix + ".host] is set but type is [local]"))); + final ClusterSettings clusterSettings = new ClusterSettings(settings, Set.of(HttpExporter.HOST_SETTING, Exporter.TYPE_SETTING)); + final SettingsException e = expectThrows(SettingsException.class, () -> clusterSettings.validate(settings, true)); + assertThat(e, hasToString(containsString("[" + prefix + ".host] is set but type is [local]"))); } public void testInvalidHost() { From ea1000898f12e3b0808ffd0621b6418701e2cb6a Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Thu, 12 Dec 2019 22:26:35 -0500 Subject: [PATCH 191/686] Respect ES_PATH_CONF on package install (#50158) We respect ES_PATH_CONF everywhere except package install. This commit addresses this by respecting ES_PATH_CONF when installing the RPM/Debian packages. --- distribution/packages/src/common/scripts/postinst | 15 +++++++++++---- distribution/packages/src/common/scripts/postrm | 12 ++++++++++-- distribution/packages/src/common/scripts/prerm | 13 ++++++++++--- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/distribution/packages/src/common/scripts/postinst b/distribution/packages/src/common/scripts/postinst index b440bb807755c..0c86904ba40e2 100644 --- a/distribution/packages/src/common/scripts/postinst +++ b/distribution/packages/src/common/scripts/postinst @@ -8,6 +8,13 @@ # $1=0 : indicates a removal # $1=1 : indicates an upgrade +# source the default env file +if [ -f "${path.env}" ]; then + . "${path.env}" +else + ES_PATH_CONF="${path.conf}" +fi + IS_UPGRADE=false case "$1" in @@ -95,11 +102,11 @@ fi # the equivalent code for rpm is in posttrans if [ "$PACKAGE" = "deb" ]; then - if [ ! -f /etc/elasticsearch/elasticsearch.keystore ]; then + if [ ! -f "${ES_PATH_CONF}"/elasticsearch.keystore ]; then /usr/share/elasticsearch/bin/elasticsearch-keystore create - chown root:elasticsearch /etc/elasticsearch/elasticsearch.keystore - chmod 660 /etc/elasticsearch/elasticsearch.keystore - md5sum /etc/elasticsearch/elasticsearch.keystore > /etc/elasticsearch/.elasticsearch.keystore.initial_md5sum + chown root:elasticsearch "${ES_PATH_CONF}"/elasticsearch.keystore + chmod 660 "${ES_PATH_CONF}"/elasticsearch.keystore + md5sum "${ES_PATH_CONF}"/elasticsearch.keystore > "${ES_PATH_CONF}"/.elasticsearch.keystore.initial_md5sum else /usr/share/elasticsearch/bin/elasticsearch-keystore upgrade fi diff --git a/distribution/packages/src/common/scripts/postrm b/distribution/packages/src/common/scripts/postrm index c54df43450af4..e0e0bd237a6ba 100644 --- a/distribution/packages/src/common/scripts/postrm +++ b/distribution/packages/src/common/scripts/postrm @@ -8,6 +8,14 @@ # On RedHat, # $1=0 : indicates a removal # $1=1 : indicates an upgrade + +# source the default env file +if [ -f "${path.env}" ]; then + . "${path.env}" +else + ES_PATH_CONF="${path.conf}" +fi + REMOVE_DIRS=false REMOVE_USER_AND_GROUP=false @@ -73,8 +81,8 @@ if [ "$REMOVE_DIRS" = "true" ]; then fi # delete the conf directory if and only if empty - if [ -d /etc/elasticsearch ]; then - rmdir --ignore-fail-on-non-empty /etc/elasticsearch + if [ -d "${ES_PATH_CONF}" ]; then + rmdir --ignore-fail-on-non-empty "${ES_PATH_CONF}" fi fi diff --git a/distribution/packages/src/common/scripts/prerm b/distribution/packages/src/common/scripts/prerm index f5cf67ca0b662..ff25345963b2d 100644 --- a/distribution/packages/src/common/scripts/prerm +++ b/distribution/packages/src/common/scripts/prerm @@ -9,6 +9,13 @@ # $1=0 : indicates a removal # $1=1 : indicates an upgrade +# source the default env file +if [ -f "${path.env}" ]; then + . "${path.env}" +else + ES_PATH_CONF="${path.conf}" +fi + STOP_REQUIRED=false REMOVE_SERVICE=false @@ -65,9 +72,9 @@ if [ "$STOP_REQUIRED" = "true" ]; then echo " OK" fi -if [ -f /etc/elasticsearch/elasticsearch.keystore ]; then - if md5sum --status -c /etc/elasticsearch/.elasticsearch.keystore.initial_md5sum; then - rm /etc/elasticsearch/elasticsearch.keystore /etc/elasticsearch/.elasticsearch.keystore.initial_md5sum +if [ -f "${ES_PATH_CONF}"/elasticsearch.keystore ]; then + if md5sum --status -c "${ES_PATH_CONF}"/.elasticsearch.keystore.initial_md5sum; then + rm "${ES_PATH_CONF}"/elasticsearch.keystore "${ES_PATH_CONF}"/.elasticsearch.keystore.initial_md5sum fi fi From a3d77da24148b3fd40449278e992bff349f69b36 Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Fri, 13 Dec 2019 08:09:25 +0100 Subject: [PATCH 192/686] [Transform] add actual timeout in message (#50140) add the timeout to the message if stopping a transform times out --- .../xpack/transform/action/TransportStopTransformAction.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java index 010cfd1cbb6f2..aaf51396abb87 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java @@ -398,7 +398,9 @@ private void waitForTransformStopped( if (stillRunningTasks.size() > 0) { message.append("Could not stop the transforms "); message.append(stillRunningTasks); - message.append(" as they timed out."); + message.append(" as they timed out ["); + message.append(timeout.toString()); + message.append("]."); } listener.onFailure(new ElasticsearchStatusException(message.toString(), RestStatus.REQUEST_TIMEOUT)); From 0f6dfe95362f452bad7480447222ad1cf839c1c9 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 13 Dec 2019 18:29:31 +1100 Subject: [PATCH 193/686] Skip enterprise license tests in release build (#50163) The release builds use a production license key, and our rest test load licenses that are signed by the dev license key. This change adds the new enterprise license Rest tests to the blacklist on release builds. Relates: #50067 Resolves: #50151 --- x-pack/plugin/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/build.gradle b/x-pack/plugin/build.gradle index 9c012df0c0b69..7663faae7c76d 100644 --- a/x-pack/plugin/build.gradle +++ b/x-pack/plugin/build.gradle @@ -69,8 +69,8 @@ integTest.runner { if (!snapshot) { // these tests attempt to install basic/internal licenses signed against the dev/public.key // Since there is no infrastructure in place (anytime soon) to generate licenses using the production - // private key, these tests are whitelisted in non-snapshot test runs - blacklist.addAll(['xpack/15_basic/*', 'license/20_put_license/*']) + // private key, these tests are blacklisted in non-snapshot test runs + blacklist.addAll(['xpack/15_basic/*', 'license/20_put_license/*', 'license/30_enterprise_license/*']) } systemProperty 'tests.rest.blacklist', blacklist.join(',') dependsOn copyKeyCerts From c0d567472369a9658e1fe9fdf5b2fe65365f4f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Fri, 13 Dec 2019 09:59:45 +0100 Subject: [PATCH 194/686] Improve DateFieldMapper `ignore_malformed` handling (#50090) A recent change around date parsing (#46675) made it stricter, so we should now also catch DateTimeExceptions in DateFieldMapper and ignore those when the `ignore_malformed` option is set. Closes #50081 --- .../index/mapper/DateFieldMapper.java | 11 +++++++++-- .../index/mapper/DateFieldMapperTests.java | 16 +++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java index 7a98d9a286ea8..43762645190e0 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java @@ -54,6 +54,7 @@ import org.elasticsearch.search.DocValueFormat; import java.io.IOException; +import java.time.DateTimeException; import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; @@ -77,19 +78,23 @@ public static class Defaults { public enum Resolution { MILLISECONDS(CONTENT_TYPE, NumericType.DATE) { + @Override public long convert(Instant instant) { return instant.toEpochMilli(); } + @Override public Instant toInstant(long value) { return Instant.ofEpochMilli(value); } }, NANOSECONDS("date_nanos", NumericType.DATE_NANOSECONDS) { + @Override public long convert(Instant instant) { return toLong(instant); } + @Override public Instant toInstant(long value) { return DateUtils.toInstant(value); } @@ -274,7 +279,9 @@ public MappedFieldType clone() { @Override public boolean equals(Object o) { - if (!super.equals(o)) return false; + if (!super.equals(o)) { + return false; + } DateFieldType that = (DateFieldType) o; return Objects.equals(dateTimeFormatter, that.dateTimeFormatter) && Objects.equals(resolution, that.resolution); } @@ -536,7 +543,7 @@ protected void parseCreateField(ParseContext context, List field long timestamp; try { timestamp = fieldType().parse(dateAsString); - } catch (IllegalArgumentException | ElasticsearchParseException e) { + } catch (IllegalArgumentException | ElasticsearchParseException | DateTimeException e) { if (ignoreMalformed.value()) { context.addIgnoredField(fieldType.name()); return; diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java index 4f7fad85326f4..daa70c865133f 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java @@ -159,7 +159,14 @@ public void testStore() throws Exception { assertEquals(1457654400000L, storedField.numericValue().longValue()); } - public void testIgnoreMalformed() throws Exception { + public void testIgnoreMalformed() throws IOException { + testIgnoreMalfomedForValue("2016-03-99", + "failed to parse date field [2016-03-99] with format [strict_date_optional_time||epoch_millis]"); + testIgnoreMalfomedForValue("-2147483648", + "Invalid value for Year (valid values -999999999 - 999999999): -2147483648"); + } + + private void testIgnoreMalfomedForValue(String value, String expectedException) throws IOException { String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") .startObject("properties").startObject("field").field("type", "date").endObject().endObject() .endObject().endObject()); @@ -171,12 +178,11 @@ public void testIgnoreMalformed() throws Exception { ThrowingRunnable runnable = () -> mapper.parse(new SourceToParse("test", "1", BytesReference .bytes(XContentFactory.jsonBuilder() .startObject() - .field("field", "2016-03-99") + .field("field", value) .endObject()), XContentType.JSON)); MapperParsingException e = expectThrows(MapperParsingException.class, runnable); - assertThat(e.getCause().getMessage(), - containsString("failed to parse date field [2016-03-99] with format [strict_date_optional_time||epoch_millis]")); + assertThat(e.getCause().getMessage(), containsString(expectedException)); mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") .startObject("properties").startObject("field").field("type", "date") @@ -188,7 +194,7 @@ public void testIgnoreMalformed() throws Exception { ParsedDocument doc = mapper2.parse(new SourceToParse("test", "1", BytesReference .bytes(XContentFactory.jsonBuilder() .startObject() - .field("field", ":1") + .field("field", value) .endObject()), XContentType.JSON)); From b4efe1fc3643c00ad5e48c2265d457b84271fd4e Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 13 Dec 2019 11:26:00 +0200 Subject: [PATCH 195/686] Fix testMalformedToken (#50164) This test was fixed as part of #49736 so that it used a TokenService mock instance that was enabled, so that token verification fails because the token is invalid and not because the token service is not enabled. When the randomly generated token we send, decodes to being of version > 7.2 , we need to have mocked a GetResponse for the call that TokenService#getUserTokenFromId will make, otherwise this hangs and times out. --- .../elasticsearch/xpack/security/authc/TokenServiceTests.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index f17bdc980bcaf..d5411ebb20b19 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -599,7 +599,9 @@ public void testMalformedToken() throws Exception { final byte[] randomBytes = new byte[numBytes]; random().nextBytes(randomBytes); TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); - + // mock another random token so that we don't find a token in TokenService#getUserTokenFromId + Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); + mockGetTokenFromId(tokenService, UUIDs.randomBase64UUID(), authentication, false); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); storeTokenHeader(requestContext, Base64.getEncoder().encodeToString(randomBytes)); From fb03630389858d69e9cde76471f626bc4f9b1dcd Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Fri, 13 Dec 2019 10:30:56 +0000 Subject: [PATCH 196/686] Remove type metadata from ingest documents (#50131) Referring to the _type field in an Ingest document has been deprecated and effectively a no-op since 7.x, and we can remove support for it entirely in 8.0 Relates to #41059 --- .../ingest/common/AppendProcessorTests.java | 2 +- .../ingest/common/SetProcessorTests.java | 2 +- .../ingest/SimulatePipelineRequest.java | 4 ---- .../elasticsearch/ingest/IngestDocument.java | 2 -- .../SimulatePipelineRequestParsingTests.java | 24 ++----------------- 5 files changed, 4 insertions(+), 30 deletions(-) diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorTests.java index 7a48c9ace326d..51b7bf0132bd4 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorTests.java @@ -126,7 +126,7 @@ public void testConvertScalarToList() throws Exception { public void testAppendMetadataExceptVersion() throws Exception { // here any metadata field value becomes a list, which won't make sense in most of the cases, // but support for append is streamlined like for set so we test it - MetaData randomMetaData = randomFrom(MetaData.INDEX, MetaData.TYPE, MetaData.ID, MetaData.ROUTING); + MetaData randomMetaData = randomFrom(MetaData.INDEX, MetaData.ID, MetaData.ROUTING); List values = new ArrayList<>(); Processor appendProcessor; if (randomBoolean()) { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorTests.java index cf698288268d2..d303501d2ea4e 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorTests.java @@ -101,7 +101,7 @@ public void testSetExistingNullFieldWithOverrideDisabled() throws Exception { } public void testSetMetadataExceptVersion() throws Exception { - MetaData randomMetaData = randomFrom(MetaData.INDEX, MetaData.TYPE, MetaData.ID, MetaData.ROUTING); + MetaData randomMetaData = randomFrom(MetaData.INDEX, MetaData.ID, MetaData.ROUTING); Processor processor = createSetProcessor(randomMetaData.getFieldName(), "_value", true); IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random()); processor.execute(ingestDocument); diff --git a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java index 884e1590d1640..949d32c8fe734 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java @@ -179,10 +179,6 @@ private static List parseDocs(Map config) { dataMap, Fields.SOURCE); String index = ConfigurationUtils.readStringOrIntProperty(null, null, dataMap, MetaData.INDEX.getFieldName(), "_index"); - if (dataMap.containsKey(MetaData.TYPE.getFieldName())) { - deprecationLogger.deprecatedAndMaybeLog("simulate_pipeline_with_types", - "[types removal] specifying _type in pipeline simulation requests is deprecated"); - } String id = ConfigurationUtils.readStringOrIntProperty(null, null, dataMap, MetaData.ID.getFieldName(), "_id"); String routing = ConfigurationUtils.readOptionalStringOrIntProperty(null, null, diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java b/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java index aabb6890a7b29..1f0d2b4757128 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java @@ -25,7 +25,6 @@ import org.elasticsearch.index.mapper.IndexFieldMapper; import org.elasticsearch.index.mapper.RoutingFieldMapper; import org.elasticsearch.index.mapper.SourceFieldMapper; -import org.elasticsearch.index.mapper.TypeFieldMapper; import org.elasticsearch.index.mapper.VersionFieldMapper; import org.elasticsearch.script.TemplateScript; @@ -692,7 +691,6 @@ public String toString() { public enum MetaData { INDEX(IndexFieldMapper.NAME), - TYPE(TypeFieldMapper.NAME), ID(IdFieldMapper.NAME), ROUTING(RoutingFieldMapper.NAME), VERSION(VersionFieldMapper.NAME), diff --git a/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java b/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java index d3965e2cf35c5..a01c1f44a73d9 100644 --- a/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java +++ b/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java @@ -43,7 +43,6 @@ import static org.elasticsearch.ingest.IngestDocument.MetaData.ID; import static org.elasticsearch.ingest.IngestDocument.MetaData.INDEX; import static org.elasticsearch.ingest.IngestDocument.MetaData.ROUTING; -import static org.elasticsearch.ingest.IngestDocument.MetaData.TYPE; import static org.elasticsearch.ingest.IngestDocument.MetaData.VERSION; import static org.elasticsearch.ingest.IngestDocument.MetaData.VERSION_TYPE; import static org.hamcrest.Matchers.equalTo; @@ -109,15 +108,7 @@ public void testParseUsingPipelineStore() throws Exception { assertThat(actualRequest.getPipeline().getProcessors().size(), equalTo(1)); } - public void testParseWithProvidedPipelineNoType() throws Exception { - innerTestParseWithProvidedPipeline(false); - } - - public void testParseWithProvidedPipelineWithType() throws Exception { - innerTestParseWithProvidedPipeline(true); - } - - private void innerTestParseWithProvidedPipeline(boolean useExplicitType) throws Exception { + public void innerTestParseWithProvidedPipeline() throws Exception { int numDocs = randomIntBetween(1, 10); Map requestContent = new HashMap<>(); @@ -127,7 +118,7 @@ private void innerTestParseWithProvidedPipeline(boolean useExplicitType) throws for (int i = 0; i < numDocs; i++) { Map doc = new HashMap<>(); Map expectedDoc = new HashMap<>(); - List fields = Arrays.asList(INDEX, TYPE, ID, ROUTING, VERSION, VERSION_TYPE); + List fields = Arrays.asList(INDEX, ID, ROUTING, VERSION, VERSION_TYPE); for(IngestDocument.MetaData field : fields) { if (field == VERSION) { Long value = randomLong(); @@ -139,14 +130,6 @@ private void innerTestParseWithProvidedPipeline(boolean useExplicitType) throws ); doc.put(field.getFieldName(), value); expectedDoc.put(field.getFieldName(), value); - } else if (field == TYPE) { - if (useExplicitType) { - String value = randomAlphaOfLengthBetween(1, 10); - doc.put(field.getFieldName(), value); - expectedDoc.put(field.getFieldName(), value); - } else { - expectedDoc.put(field.getFieldName(), "_doc"); - } } else { if (randomBoolean()) { String value = randomAlphaOfLengthBetween(1, 10); @@ -213,9 +196,6 @@ private void innerTestParseWithProvidedPipeline(boolean useExplicitType) throws assertThat(actualRequest.getPipeline().getId(), equalTo(SIMULATED_PIPELINE_ID)); assertThat(actualRequest.getPipeline().getDescription(), nullValue()); assertThat(actualRequest.getPipeline().getProcessors().size(), equalTo(numProcessors)); - if (useExplicitType) { - assertWarnings("[types removal] specifying _type in pipeline simulation requests is deprecated"); - } } public void testNullPipelineId() { From da73b00b4119513cd53f9f0d12180e97f52cdaef Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Fri, 13 Dec 2019 08:43:35 -0500 Subject: [PATCH 197/686] [DOCS] Reformat token count limit filter docs (#49835) --- .../limit-token-count-tokenfilter.asciidoc | 134 +++++++++++++++--- 1 file changed, 118 insertions(+), 16 deletions(-) diff --git a/docs/reference/analysis/tokenfilters/limit-token-count-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/limit-token-count-tokenfilter.asciidoc index 7fe2432ca5473..61e7ec8706d88 100644 --- a/docs/reference/analysis/tokenfilters/limit-token-count-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/limit-token-count-tokenfilter.asciidoc @@ -1,32 +1,134 @@ [[analysis-limit-token-count-tokenfilter]] -=== Limit Token Count Token Filter +=== Limit token count token filter +++++ +Limit token count +++++ -Limits the number of tokens that are indexed per document and field. +Limits the number of output tokens. The `limit` filter is commonly used to limit +the size of document field values based on token count. -[cols="<,<",options="header",] -|======================================================================= -|Setting |Description -|`max_token_count` |The maximum number of tokens that should be indexed -per document and field. The default is `1` +By default, the `limit` filter keeps only the first token in a stream. For +example, the filter can change the token stream `[ one, two, three ]` to +`[ one ]`. -|`consume_all_tokens` |If set to `true` the filter exhaust the stream -even if `max_token_count` tokens have been consumed already. The default -is `false`. -|======================================================================= +This filter uses Lucene's +https://lucene.apache.org/core/{lucene_version_path}/analyzers-common/org/apache/lucene/analysis/miscellaneous/LimitTokenCountFilter.html[LimitTokenCountFilter]. -Here is an example: +[TIP] +==== + If you want to limit the size of field values based on +_character length_, use the <> mapping parameter. +==== + +[[analysis-limit-token-count-tokenfilter-configure-parms]] +==== Configurable parameters + +`max_token_count`:: +(Optional, integer) +Maximum number of tokens to keep. Once this limit is reached, any remaining +tokens are excluded from the output. Defaults to `1`. + +`consume_all_tokens`:: +(Optional, boolean) +If `true`, the `limit` filter exhausts the token stream, even if the +`max_token_count` has already been reached. Defaults to `false`. + +[[analysis-limit-token-count-tokenfilter-analyze-ex]] +==== Example + +The following <> request uses the `limit` +filter to keep only the first two tokens in `quick fox jumps over lazy dog`: + +[source,console] +-------------------------------------------------- +GET _analyze +{ + "tokenizer": "standard", + "filter": [ + { + "type": "limit", + "max_token_count": 2 + } + ], + "text": "quick fox jumps over lazy dog" +} +-------------------------------------------------- + +The filter produces the following tokens: + +[source,text] +-------------------------------------------------- +[ quick, fox ] +-------------------------------------------------- + +///////////////////// +[source,console-result] +-------------------------------------------------- +{ + "tokens": [ + { + "token": "quick", + "start_offset": 0, + "end_offset": 5, + "type": "", + "position": 0 + }, + { + "token": "fox", + "start_offset": 6, + "end_offset": 9, + "type": "", + "position": 1 + } + ] +} +-------------------------------------------------- +///////////////////// + +[[analysis-limit-token-count-tokenfilter-analyzer-ex]] +==== Add to an analyzer + +The following <> request uses the +`limit` filter to configure a new +<>. [source,console] -------------------------------------------------- -PUT /limit_example +PUT limit_example { "settings": { "analysis": { "analyzer": { - "limit_example": { - "type": "custom", + "standard_one_token_limit": { "tokenizer": "standard", - "filter": ["lowercase", "five_token_limit"] + "filter": [ "limit" ] + } + } + } + } +} +-------------------------------------------------- + +[[analysis-limit-token-count-tokenfilter-customize]] +==== Customize + +To customize the `limit` filter, duplicate it to create the basis +for a new custom token filter. You can modify the filter using its configurable +parameters. + +For example, the following request creates a custom `limit` filter that keeps +only the first five tokens of a stream: + +[source,console] +-------------------------------------------------- +PUT custom_limit_example +{ + "settings": { + "analysis": { + "analyzer": { + "whitespace_five_token_limit": { + "tokenizer": "whitespace", + "filter": [ "five_token_limit" ] } }, "filter": { From 9648fbd838f4a0ea95fdbb16656f58411e07fd00 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 13 Dec 2019 09:10:50 -0500 Subject: [PATCH 198/686] [ML][Inference] Adding a warning_field for warning msgs. (#49838) This adds a new field for the inference processor. `warning_field` is a place for us to write warnings provided from the inference call. When there are warnings we are not going to write an inference result. The goal of this is to indicate that the data provided was too poor or too different for the model to make an accurate prediction. The user could optionally include the `warning_field`. When it is not provided, it is assumed no warnings were desired to be written. The first of these warnings is when ALL of the input fields are missing. If none of the trained fields are present, we don't bother inferencing against the model and instead provide a warning stating that the fields were missing. Also, this adds checks to not allow duplicated fields during processor creation. --- .../results/WarningInferenceResults.java | 66 +++++++++++++++++++ .../xpack/core/ml/job/messages/Messages.java | 1 + .../results/WarningInferenceResultsTests.java | 39 +++++++++++ .../process/AnalyticsResultProcessor.java | 20 +++++- .../inference/ingest/InferenceProcessor.java | 43 ++++++++++-- .../inference/loadingservice/LocalModel.java | 17 ++++- .../loadingservice/ModelLoadingService.java | 6 +- .../AnalyticsResultProcessorTests.java | 2 +- .../InferenceProcessorFactoryTests.java | 23 +++++++ .../ingest/InferenceProcessorTests.java | 23 +++++++ .../loadingservice/LocalModelTests.java | 36 ++++++++-- .../ModelLoadingServiceTests.java | 3 + .../integration/ModelInferenceActionIT.java | 52 ++++++++++++++- 13 files changed, 315 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/WarningInferenceResults.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/WarningInferenceResultsTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/WarningInferenceResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/WarningInferenceResults.java new file mode 100644 index 0000000000000..a052c2b263d3e --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/results/WarningInferenceResults.java @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.inference.results; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; + +import java.io.IOException; +import java.util.Objects; + +public class WarningInferenceResults implements InferenceResults { + + public static final String NAME = "warning"; + public static final ParseField WARNING = new ParseField("warning"); + + private final String warning; + + public WarningInferenceResults(String warning) { + this.warning = warning; + } + + public WarningInferenceResults(StreamInput in) throws IOException { + this.warning = in.readString(); + } + + public String getWarning() { + return warning; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(warning); + } + + @Override + public boolean equals(Object object) { + if (object == this) { return true; } + if (object == null || getClass() != object.getClass()) { return false; } + WarningInferenceResults that = (WarningInferenceResults) object; + return Objects.equals(warning, that.warning); + } + + @Override + public int hashCode() { + return Objects.hash(warning); + } + + @Override + public void writeResult(IngestDocument document, String parentResultField) { + ExceptionsHelper.requireNonNull(document, "document"); + ExceptionsHelper.requireNonNull(parentResultField, "resultField"); + document.setFieldValue(parentResultField + "." + "warning", warning); + } + + @Override + public String getWriteableName() { + return NAME; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java index febe9a8eb42a9..5e7d3ee3318c6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java @@ -92,6 +92,7 @@ public final class Messages { public static final String INFERENCE_FAILED_TO_DESERIALIZE = "Could not deserialize trained model [{0}]"; public static final String INFERENCE_TO_MANY_DEFINITIONS_REQUESTED = "Getting model definition is not supported when getting more than one model"; + public static final String INFERENCE_WARNING_ALL_FIELDS_MISSING = "Model [{0}] could not be inferred as all fields were missing"; public static final String JOB_AUDIT_DATAFEED_DATA_SEEN_AGAIN = "Datafeed has started retrieving data again"; public static final String JOB_AUDIT_CREATED = "Job created"; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/WarningInferenceResultsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/WarningInferenceResultsTests.java new file mode 100644 index 0000000000000..da48a91cdde38 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/results/WarningInferenceResultsTests.java @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.inference.results; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.util.HashMap; + +import static org.hamcrest.Matchers.equalTo; + +public class WarningInferenceResultsTests extends AbstractWireSerializingTestCase { + + public static WarningInferenceResults createRandomResults() { + return new WarningInferenceResults(randomAlphaOfLength(10)); + } + + public void testWriteResults() { + WarningInferenceResults result = new WarningInferenceResults("foo"); + IngestDocument document = new IngestDocument(new HashMap<>(), new HashMap<>()); + result.writeResult(document, "result_field"); + + assertThat(document.getFieldValue("result_field.warning", String.class), equalTo("foo")); + } + + @Override + protected WarningInferenceResults createTestInstance() { + return createRandomResults(); + } + + @Override + protected Writeable.Reader instanceReader() { + return WarningInferenceResults::new; + } +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessor.java index b6ac92134723c..00fea87a05b20 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessor.java @@ -16,6 +16,8 @@ import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.license.License; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.Classification; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.Regression; import org.elasticsearch.xpack.core.ml.inference.TrainedModelConfig; import org.elasticsearch.xpack.core.ml.inference.TrainedModelDefinition; import org.elasticsearch.xpack.core.ml.inference.TrainedModelInput; @@ -34,6 +36,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import static java.util.stream.Collectors.toList; + public class AnalyticsResultProcessor { private static final Logger LOGGER = LogManager.getLogger(AnalyticsResultProcessor.class); @@ -163,6 +167,10 @@ private TrainedModelConfig createTrainedModelConfig(TrainedModelDefinition.Build Instant createTime = Instant.now(); String modelId = analytics.getId() + "-" + createTime.toEpochMilli(); TrainedModelDefinition definition = inferenceModel.build(); + String dependentVariable = getDependentVariable(); + List fieldNamesWithoutDependentVariable = fieldNames.stream() + .filter(f -> f.equals(dependentVariable) == false) + .collect(toList()); return TrainedModelConfig.builder() .setModelId(modelId) .setCreatedBy("data-frame-analytics") @@ -175,11 +183,21 @@ private TrainedModelConfig createTrainedModelConfig(TrainedModelDefinition.Build .setEstimatedHeapMemory(definition.ramBytesUsed()) .setEstimatedOperations(definition.getTrainedModel().estimatedNumOperations()) .setParsedDefinition(inferenceModel) - .setInput(new TrainedModelInput(fieldNames)) + .setInput(new TrainedModelInput(fieldNamesWithoutDependentVariable)) .setLicenseLevel(License.OperationMode.PLATINUM.description()) .build(); } + private String getDependentVariable() { + if (analytics.getAnalysis() instanceof Classification) { + return ((Classification)analytics.getAnalysis()).getDependentVariable(); + } + if (analytics.getAnalysis() instanceof Regression) { + return ((Regression)analytics.getAnalysis()).getDependentVariable(); + } + return null; + } + private CountDownLatch storeTrainedModel(TrainedModelConfig trainedModelConfig) { CountDownLatch latch = new CountDownLatch(1); ActionListener storeListener = ActionListener.wrap( diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java index 805123cf53cc2..19c0054b522bf 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java @@ -28,6 +28,8 @@ import org.elasticsearch.ingest.Processor; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.ml.action.InternalInferModelAction; +import org.elasticsearch.xpack.core.ml.inference.results.InferenceResults; +import org.elasticsearch.xpack.core.ml.inference.results.WarningInferenceResults; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ClassificationConfig; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.RegressionConfig; @@ -37,7 +39,9 @@ import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -146,7 +150,12 @@ void mutateDocument(InternalInferModelAction.Response response, IngestDocument i if (response.getInferenceResults().isEmpty()) { throw new ElasticsearchStatusException("Unexpected empty inference response", RestStatus.INTERNAL_SERVER_ERROR); } - response.getInferenceResults().get(0).writeResult(ingestDocument, this.targetField); + InferenceResults inferenceResults = response.getInferenceResults().get(0); + if (inferenceResults instanceof WarningInferenceResults) { + inferenceResults.writeResult(ingestDocument, this.targetField); + } else { + response.getInferenceResults().get(0).writeResult(ingestDocument, this.targetField); + } ingestDocument.setFieldValue(targetField + "." + MODEL_ID, modelId); } @@ -164,6 +173,10 @@ public static final class Factory implements Processor.Factory, Consumer RESERVED_ML_FIELD_NAMES = new HashSet<>(Arrays.asList( + WarningInferenceResults.WARNING.getPreferredName(), + MODEL_ID)); + private final Client client; private final IngestService ingestService; private final InferenceAuditor auditor; @@ -235,6 +248,7 @@ public InferenceProcessor create(Map processorFactori String targetField = ConfigurationUtils.readStringProperty(TYPE, tag, config, TARGET_FIELD, defaultTargetField); Map fieldMapping = ConfigurationUtils.readOptionalMap(TYPE, tag, config, FIELD_MAPPINGS); InferenceConfig inferenceConfig = inferenceConfigFromMap(ConfigurationUtils.readMap(TYPE, tag, config, INFERENCE_CONFIG)); + return new InferenceProcessor(client, auditor, tag, @@ -252,7 +266,6 @@ void setMaxIngestProcessors(int maxIngestProcessors) { InferenceConfig inferenceConfigFromMap(Map inferenceConfig) { ExceptionsHelper.requireNonNull(inferenceConfig, INFERENCE_CONFIG); - if (inferenceConfig.size() != 1) { throw ExceptionsHelper.badRequestException("{} must be an object with one inference type mapped to an object.", INFERENCE_CONFIG); @@ -268,10 +281,14 @@ InferenceConfig inferenceConfigFromMap(Map inferenceConfig) { if (inferenceConfig.containsKey(ClassificationConfig.NAME)) { checkSupportedVersion(ClassificationConfig.EMPTY_PARAMS); - return ClassificationConfig.fromMap(valueMap); + ClassificationConfig config = ClassificationConfig.fromMap(valueMap); + checkFieldUniqueness(config.getResultsField(), config.getTopClassesResultsField()); + return config; } else if (inferenceConfig.containsKey(RegressionConfig.NAME)) { checkSupportedVersion(RegressionConfig.EMPTY_PARAMS); - return RegressionConfig.fromMap(valueMap); + RegressionConfig config = RegressionConfig.fromMap(valueMap); + checkFieldUniqueness(config.getResultsField()); + return config; } else { throw ExceptionsHelper.badRequestException("unrecognized inference configuration type {}. Supported types {}", inferenceConfig.keySet(), @@ -279,6 +296,23 @@ InferenceConfig inferenceConfigFromMap(Map inferenceConfig) { } } + private static void checkFieldUniqueness(String... fieldNames) { + Set duplicatedFieldNames = new HashSet<>(); + Set currentFieldNames = new HashSet<>(RESERVED_ML_FIELD_NAMES); + for(String fieldName : fieldNames) { + if (currentFieldNames.contains(fieldName)) { + duplicatedFieldNames.add(fieldName); + } else { + currentFieldNames.add(fieldName); + } + } + if (duplicatedFieldNames.isEmpty() == false) { + throw ExceptionsHelper.badRequestException("Cannot create processor as configured." + + " More than one field is configured as {}", + duplicatedFieldNames); + } + } + void checkSupportedVersion(InferenceConfig config) { if (config.getMinimalSupportedVersion().after(minNodeVersion)) { throw ExceptionsHelper.badRequestException(Messages.getMessage(Messages.INFERENCE_CONFIG_NOT_SUPPORTED_ON_VERSION, @@ -287,6 +321,5 @@ void checkSupportedVersion(InferenceConfig config) { minNodeVersion)); } } - } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModel.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModel.java index 403f10dd7d83b..4e62c69336b6a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModel.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModel.java @@ -6,23 +6,33 @@ package org.elasticsearch.xpack.ml.inference.loadingservice; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.xpack.core.ml.inference.TrainedModelDefinition; +import org.elasticsearch.xpack.core.ml.inference.TrainedModelInput; +import org.elasticsearch.xpack.core.ml.inference.results.WarningInferenceResults; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; +import org.elasticsearch.xpack.core.ml.job.messages.Messages; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import org.elasticsearch.xpack.core.ml.inference.results.ClassificationInferenceResults; import org.elasticsearch.xpack.core.ml.inference.results.InferenceResults; import org.elasticsearch.xpack.core.ml.inference.results.RegressionInferenceResults; +import java.util.HashSet; import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.xpack.core.ml.job.messages.Messages.INFERENCE_WARNING_ALL_FIELDS_MISSING; public class LocalModel implements Model { private final TrainedModelDefinition trainedModelDefinition; private final String modelId; + private final Set fieldNames; - public LocalModel(String modelId, TrainedModelDefinition trainedModelDefinition) { + public LocalModel(String modelId, TrainedModelDefinition trainedModelDefinition, TrainedModelInput input) { this.trainedModelDefinition = trainedModelDefinition; this.modelId = modelId; + this.fieldNames = new HashSet<>(input.getFieldNames()); } long ramBytesUsed() { @@ -51,6 +61,11 @@ public String getResultsType() { @Override public void infer(Map fields, InferenceConfig config, ActionListener listener) { try { + if (Sets.haveEmptyIntersection(fieldNames, fields.keySet())) { + listener.onResponse(new WarningInferenceResults(Messages.getMessage(INFERENCE_WARNING_ALL_FIELDS_MISSING, modelId))); + return; + } + listener.onResponse(trainedModelDefinition.infer(fields, config)); } catch (Exception e) { listener.onFailure(e); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/ModelLoadingService.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/ModelLoadingService.java index 2355228f4c366..b5862acfefc27 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/ModelLoadingService.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/ModelLoadingService.java @@ -141,7 +141,8 @@ public void getModel(String modelId, ActionListener modelActionListener) trainedModelConfig -> modelActionListener.onResponse(new LocalModel( trainedModelConfig.getModelId(), - trainedModelConfig.ensureParsedDefinition(namedXContentRegistry).getModelDefinition())), + trainedModelConfig.ensureParsedDefinition(namedXContentRegistry).getModelDefinition(), + trainedModelConfig.getInput())), modelActionListener::onFailure )); } else { @@ -198,7 +199,8 @@ private void handleLoadSuccess(String modelId, TrainedModelConfig trainedModelCo Queue> listeners; LocalModel loadedModel = new LocalModel( trainedModelConfig.getModelId(), - trainedModelConfig.ensureParsedDefinition(namedXContentRegistry).getModelDefinition()); + trainedModelConfig.ensureParsedDefinition(namedXContentRegistry).getModelDefinition(), + trainedModelConfig.getInput()); synchronized (loadingListeners) { listeners = loadingListeners.remove(modelId); // If there is no loadingListener that means the loading was canceled and the listener was already notified as such diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java index 15bd32da3c320..036023eb8c9aa 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsResultProcessorTests.java @@ -171,7 +171,7 @@ public void testProcess_GivenInferenceModelIsStoredSuccessfully() { assertThat(storedModel.getTags(), contains(JOB_ID)); assertThat(storedModel.getDescription(), equalTo(JOB_DESCRIPTION)); assertThat(storedModel.getModelDefinition(), equalTo(inferenceModel.build())); - assertThat(storedModel.getInput().getFieldNames(), equalTo(expectedFieldNames)); + assertThat(storedModel.getInput().getFieldNames(), equalTo(Arrays.asList("bar", "baz"))); assertThat(storedModel.getEstimatedHeapMemory(), equalTo(inferenceModel.build().ramBytesUsed())); assertThat(storedModel.getEstimatedOperations(), equalTo(inferenceModel.build().getTrainedModel().estimatedNumOperations())); Map metadata = storedModel.getMetadata(); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorFactoryTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorFactoryTests.java index 6f5011d0497cd..f7216ab206e85 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorFactoryTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorFactoryTests.java @@ -238,6 +238,29 @@ public void testCreateProcessor() { } } + public void testCreateProcessorWithDuplicateFields() { + InferenceProcessor.Factory processorFactory = new InferenceProcessor.Factory(client, + clusterService, + Settings.EMPTY, + ingestService); + + Map regression = new HashMap<>() {{ + put(InferenceProcessor.FIELD_MAPPINGS, Collections.emptyMap()); + put(InferenceProcessor.MODEL_ID, "my_model"); + put(InferenceProcessor.TARGET_FIELD, "ml"); + put(InferenceProcessor.INFERENCE_CONFIG, Collections.singletonMap(RegressionConfig.NAME, + Collections.singletonMap(RegressionConfig.RESULTS_FIELD.getPreferredName(), "warning"))); + }}; + + try { + processorFactory.create(Collections.emptyMap(), "my_inference_processor", regression); + fail("should not have succeeded creating with duplicate fields"); + } catch (Exception ex) { + assertThat(ex.getMessage(), equalTo("Cannot create processor as configured. " + + "More than one field is configured as [warning]")); + } + } + private static ClusterState buildClusterState(MetaData metaData) { return ClusterState.builder(new ClusterName("_name")).metaData(metaData).build(); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java index 55720f73ccb25..014c9fad11203 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.xpack.core.ml.action.InternalInferModelAction; import org.elasticsearch.xpack.core.ml.inference.results.ClassificationInferenceResults; import org.elasticsearch.xpack.core.ml.inference.results.RegressionInferenceResults; +import org.elasticsearch.xpack.core.ml.inference.results.WarningInferenceResults; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ClassificationConfig; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.RegressionConfig; import org.elasticsearch.xpack.ml.notifications.InferenceAuditor; @@ -253,4 +254,26 @@ public void testHandleResponseLicenseChanged() { verify(auditor, times(1)).warning(eq("regression_model"), any(String.class)); } + public void testMutateDocumentWithWarningResult() { + String targetField = "regression_value"; + InferenceProcessor inferenceProcessor = new InferenceProcessor(client, + auditor, + "my_processor", + "ml", + "regression_model", + RegressionConfig.EMPTY_PARAMS, + Collections.emptyMap()); + + Map source = new HashMap<>(); + Map ingestMetadata = new HashMap<>(); + IngestDocument document = new IngestDocument(source, ingestMetadata); + + InternalInferModelAction.Response response = new InternalInferModelAction.Response( + Collections.singletonList(new WarningInferenceResults("something broke")), true); + inferenceProcessor.mutateDocument(response, document); + + assertThat(document.hasField(targetField), is(false)); + assertThat(document.hasField("ml.warning"), is(true)); + assertThat(document.hasField("ml.my_processor"), is(false)); + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java index 41bad95fbb3a6..fb82c4ddfaaf5 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java @@ -8,8 +8,10 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.ml.inference.TrainedModelDefinition; +import org.elasticsearch.xpack.core.ml.inference.TrainedModelInput; import org.elasticsearch.xpack.core.ml.inference.preprocessing.OneHotEncoding; import org.elasticsearch.xpack.core.ml.inference.results.SingleValueInferenceResults; +import org.elasticsearch.xpack.core.ml.inference.results.WarningInferenceResults; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ClassificationConfig; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.RegressionConfig; @@ -22,6 +24,7 @@ import org.elasticsearch.xpack.core.ml.inference.trainedmodel.tree.TreeNode; import org.elasticsearch.xpack.core.ml.inference.results.ClassificationInferenceResults; import org.elasticsearch.xpack.core.ml.inference.results.InferenceResults; +import org.elasticsearch.xpack.core.ml.job.messages.Messages; import java.util.Arrays; import java.util.HashMap; @@ -38,12 +41,13 @@ public class LocalModelTests extends ESTestCase { public void testClassificationInfer() throws Exception { String modelId = "classification_model"; + List inputFields = Arrays.asList("foo", "bar", "categorical"); TrainedModelDefinition definition = new TrainedModelDefinition.Builder() .setPreProcessors(Arrays.asList(new OneHotEncoding("categorical", oneHotMap()))) .setTrainedModel(buildClassification(false)) .build(); - Model model = new LocalModel(modelId, definition); + Model model = new LocalModel(modelId, definition, new TrainedModelInput(inputFields)); Map fields = new HashMap<>() {{ put("foo", 1.0); put("bar", 0.5); @@ -64,7 +68,7 @@ public void testClassificationInfer() throws Exception { .setPreProcessors(Arrays.asList(new OneHotEncoding("categorical", oneHotMap()))) .setTrainedModel(buildClassification(true)) .build(); - model = new LocalModel(modelId, definition); + model = new LocalModel(modelId, definition, new TrainedModelInput(inputFields)); result = getSingleValue(model, fields, new ClassificationConfig(0)); assertThat(result.value(), equalTo(0.0)); assertThat(result.valueAsString(), equalTo("not_to_be")); @@ -81,11 +85,12 @@ public void testClassificationInfer() throws Exception { } public void testRegression() throws Exception { + List inputFields = Arrays.asList("foo", "bar", "categorical"); TrainedModelDefinition trainedModelDefinition = new TrainedModelDefinition.Builder() .setPreProcessors(Arrays.asList(new OneHotEncoding("categorical", oneHotMap()))) .setTrainedModel(buildRegression()) .build(); - Model model = new LocalModel("regression_model", trainedModelDefinition); + Model model = new LocalModel("regression_model", trainedModelDefinition, new TrainedModelInput(inputFields)); Map fields = new HashMap<>() {{ put("foo", 1.0); @@ -103,12 +108,35 @@ public void testRegression() throws Exception { equalTo("Cannot infer using configuration for [classification] when model target_type is [regression]")); } + public void testAllFieldsMissing() throws Exception { + List inputFields = Arrays.asList("foo", "bar", "categorical"); + TrainedModelDefinition trainedModelDefinition = new TrainedModelDefinition.Builder() + .setPreProcessors(Arrays.asList(new OneHotEncoding("categorical", oneHotMap()))) + .setTrainedModel(buildRegression()) + .build(); + Model model = new LocalModel("regression_model", trainedModelDefinition, new TrainedModelInput(inputFields)); + + Map fields = new HashMap<>() {{ + put("something", 1.0); + put("other", 0.5); + put("baz", "dog"); + }}; + + WarningInferenceResults results = (WarningInferenceResults)getInferenceResult(model, fields, RegressionConfig.EMPTY_PARAMS); + assertThat(results.getWarning(), + equalTo(Messages.getMessage(Messages.INFERENCE_WARNING_ALL_FIELDS_MISSING, "regression_model"))); + } + private static SingleValueInferenceResults getSingleValue(Model model, Map fields, InferenceConfig config) throws Exception { + return (SingleValueInferenceResults)getInferenceResult(model, fields, config); + } + + private static InferenceResults getInferenceResult(Model model, Map fields, InferenceConfig config) throws Exception { PlainActionFuture future = new PlainActionFuture<>(); model.infer(fields, config, future); - return (SingleValueInferenceResults)future.get(); + return future.get(); } private static Map oneHotMap() { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/ModelLoadingServiceTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/ModelLoadingServiceTests.java index 462a9a90527ba..85c34c6f50496 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/ModelLoadingServiceTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/ModelLoadingServiceTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ml.inference.TrainedModelConfig; import org.elasticsearch.xpack.core.ml.inference.TrainedModelDefinition; +import org.elasticsearch.xpack.core.ml.inference.TrainedModelInput; import org.elasticsearch.xpack.core.ml.job.messages.Messages; import org.elasticsearch.xpack.ml.inference.ingest.InferenceProcessor; import org.elasticsearch.xpack.ml.inference.persistence.TrainedModelProvider; @@ -45,6 +46,7 @@ import java.io.IOException; import java.net.InetAddress; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -308,6 +310,7 @@ private void withTrainedModel(String modelId, long size) throws IOException { when(definition.ramBytesUsed()).thenReturn(size); TrainedModelConfig trainedModelConfig = mock(TrainedModelConfig.class); when(trainedModelConfig.getModelDefinition()).thenReturn(definition); + when(trainedModelConfig.getInput()).thenReturn(new TrainedModelInput(Arrays.asList("foo", "bar", "baz"))); doAnswer(invocationOnMock -> { @SuppressWarnings("rawtypes") ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java index eb1064aebb21c..6f48ce7d3e745 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java @@ -17,7 +17,9 @@ import org.elasticsearch.xpack.core.ml.inference.TrainedModelDefinitionTests; import org.elasticsearch.xpack.core.ml.inference.TrainedModelInput; import org.elasticsearch.xpack.core.ml.inference.preprocessing.OneHotEncoding; +import org.elasticsearch.xpack.core.ml.inference.results.InferenceResults; import org.elasticsearch.xpack.core.ml.inference.results.SingleValueInferenceResults; +import org.elasticsearch.xpack.core.ml.inference.results.WarningInferenceResults; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ClassificationConfig; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.RegressionConfig; import org.elasticsearch.xpack.core.ml.job.messages.Messages; @@ -39,6 +41,7 @@ import static org.elasticsearch.xpack.ml.inference.loadingservice.LocalModelTests.buildClassification; import static org.elasticsearch.xpack.ml.inference.loadingservice.LocalModelTests.buildRegression; +import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; @@ -63,7 +66,7 @@ public void testInferModels() throws Exception { oneHotEncoding.put("cat", "animal_cat"); oneHotEncoding.put("dog", "animal_dog"); TrainedModelConfig config1 = buildTrainedModelConfigBuilder(modelId2) - .setInput(new TrainedModelInput(Arrays.asList("field1", "field2"))) + .setInput(new TrainedModelInput(Arrays.asList("foo", "bar", "categorical"))) .setParsedDefinition(new TrainedModelDefinition.Builder() .setPreProcessors(Arrays.asList(new OneHotEncoding("categorical", oneHotEncoding))) .setTrainedModel(buildClassification(true))) @@ -74,7 +77,7 @@ public void testInferModels() throws Exception { .setEstimatedHeapMemory(0) .build(); TrainedModelConfig config2 = buildTrainedModelConfigBuilder(modelId1) - .setInput(new TrainedModelInput(Arrays.asList("field1", "field2"))) + .setInput(new TrainedModelInput(Arrays.asList("foo", "bar", "categorical"))) .setParsedDefinition(new TrainedModelDefinition.Builder() .setPreProcessors(Arrays.asList(new OneHotEncoding("categorical", oneHotEncoding))) .setTrainedModel(buildRegression())) @@ -184,6 +187,51 @@ public void testInferMissingModel() { } } + public void testInferMissingFields() throws Exception { + String modelId = "test-load-models-regression-missing-fields"; + Map oneHotEncoding = new HashMap<>(); + oneHotEncoding.put("cat", "animal_cat"); + oneHotEncoding.put("dog", "animal_dog"); + TrainedModelConfig config = buildTrainedModelConfigBuilder(modelId) + .setInput(new TrainedModelInput(Arrays.asList("field1", "field2"))) + .setParsedDefinition(new TrainedModelDefinition.Builder() + .setPreProcessors(Arrays.asList(new OneHotEncoding("categorical", oneHotEncoding))) + .setTrainedModel(buildRegression())) + .setVersion(Version.CURRENT) + .setEstimatedOperations(0) + .setEstimatedHeapMemory(0) + .setCreateTime(Instant.now()) + .build(); + AtomicReference putConfigHolder = new AtomicReference<>(); + AtomicReference exceptionHolder = new AtomicReference<>(); + + blockingCall(listener -> trainedModelProvider.storeTrainedModel(config, listener), putConfigHolder, exceptionHolder); + assertThat(putConfigHolder.get(), is(true)); + assertThat(exceptionHolder.get(), is(nullValue())); + + + List> toInferMissingField = new ArrayList<>(); + toInferMissingField.add(new HashMap<>() {{ + put("foo", 1.0); + put("bar", 0.5); + }}); + + InternalInferModelAction.Request request = new InternalInferModelAction.Request( + modelId, + toInferMissingField, + RegressionConfig.EMPTY_PARAMS, + true); + try { + InferenceResults result = + client().execute(InternalInferModelAction.INSTANCE, request).actionGet().getInferenceResults().get(0); + assertThat(result, is(instanceOf(WarningInferenceResults.class))); + assertThat(((WarningInferenceResults)result).getWarning(), + equalTo(Messages.getMessage(Messages.INFERENCE_WARNING_ALL_FIELDS_MISSING, modelId))); + } catch (ElasticsearchException ex) { + fail("Should not have thrown. Ex: " + ex.getMessage()); + } + } + private static TrainedModelConfig.Builder buildTrainedModelConfigBuilder(String modelId) { return TrainedModelConfig.builder() .setCreatedBy("ml_test") From 358b2e242754c351af60f4756f962d62ef42912b Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 13 Dec 2019 16:11:30 +0200 Subject: [PATCH 199/686] [ML] Fix DFA explain API timeout when source index is missing (#50176) This commit fixes a bug that caused the data frame analytics _explain API to time out in a multi-node setup when the source index was missing. When we try to create the extracted fields detector, we check the index settings. If the index is missing that responds with a failure that could be wrapped as a remote exception. While we unwrapped correctly to check if the cause was an `IndexNotFoundException`, we then proceeded to cast the original exception instead of the cause. --- .../ml/integration/ExplainDataFrameAnalyticsIT.java | 13 +++++++++++++ .../extractor/ExtractedFieldsDetectorFactory.java | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java index ba00e49456f5f..32ee2d28c865e 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ExplainDataFrameAnalyticsIT.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.ml.integration; +import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.index.IndexRequest; @@ -14,14 +15,26 @@ import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsSource; import org.elasticsearch.xpack.core.ml.dataframe.analyses.Classification; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.OutlierDetection; import org.elasticsearch.xpack.core.ml.utils.QueryProvider; import java.io.IOException; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; public class ExplainDataFrameAnalyticsIT extends MlNativeDataFrameAnalyticsIntegTestCase { + public void testExplain_GivenMissingSourceIndex() { + DataFrameAnalyticsConfig config = new DataFrameAnalyticsConfig.Builder() + .setSource(new DataFrameAnalyticsSource(new String[] {"missing_index"}, null, null)) + .setAnalysis(new OutlierDetection.Builder().build()) + .buildForExplain(); + + ResourceNotFoundException e = expectThrows(ResourceNotFoundException.class, () -> explainDataFrame(config)); + assertThat(e.getMessage(), equalTo("cannot retrieve data because index [missing_index] does not exist")); + } + public void testSourceQueryIsApplied() throws IOException { // To test the source query is applied when we extract data, // we set up a job where we have a query which excludes all but one document. diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java index 8e6ad7a614b09..c44555921cf38 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java @@ -171,9 +171,10 @@ private void getDocValueFieldsLimit(String[] index, ActionListener docV docValueFieldsLimitListener.onResponse(minDocValueFieldsLimit); }, e -> { - if (ExceptionsHelper.unwrapCause(e) instanceof IndexNotFoundException) { + Throwable cause = ExceptionsHelper.unwrapCause(e); + if (cause instanceof IndexNotFoundException) { docValueFieldsLimitListener.onFailure(new ResourceNotFoundException("cannot retrieve data because index " - + ((IndexNotFoundException) e).getIndex() + " does not exist")); + + ((IndexNotFoundException) cause).getIndex() + " does not exist")); } else { docValueFieldsLimitListener.onFailure(e); } From fdf820d3badebc487170d5d874ea11055fac2373 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 13 Dec 2019 16:16:54 +0200 Subject: [PATCH 200/686] Replace health calls with authenticate calls (#50073) In integration tests for API Keys and Tokens, we would use calls we did with the transport client to the cluster health endpoint after adding the API key or Token with `filterWithHeader()` in order to verify that the API Key or Token is valid. The response would always be successful regardless of the validity of the Token or API Key since the internal request would have the `_system` user as a fallback user and the `_system` is allowed to call the health API. When failing to validate the token or key, we would fallback to the `_system` user, see AuthenticationService#handleNullToken This commit changes our behavior to use the RestClient and call the authenticate API to verify the validity of tokens and API keys. --- .../security/authc/ApiKeyIntegTests.java | 23 +++++++++++------- .../security/authc/TokenAuthIntegTests.java | 24 +++++++++---------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index 09e2114a8f0c4..e15b5d7d3d00d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -9,12 +9,14 @@ import com.google.common.collect.Sets; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.DocWriteResponse; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; import org.elasticsearch.action.admin.indices.refresh.RefreshResponse; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.Client; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.security.AuthenticateResponse; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -77,6 +79,11 @@ public Settings nodeSettings(int nodeOrdinal) { .build(); } + @Override + protected boolean addMockHttpTransport() { + return false; // need real http + } + @Before public void waitForSecurityIndexWritable() throws Exception { assertSecurityIndexActive(); @@ -125,7 +132,7 @@ private void awaitApiKeysRemoverCompletion() throws Exception { } @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/47958") - public void testCreateApiKey() { + public void testCreateApiKey() throws Exception{ final Instant start = Instant.now(); final RoleDescriptor descriptor = new RoleDescriptor("role", new String[] { "monitor" }, null, null); Client client = client().filterWithHeader(Collections.singletonMap("Authorization", @@ -155,13 +162,11 @@ public void testCreateApiKey() { // use the first ApiKey for authorized action final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( (response.getId() + ":" + response.getKey().toString()).getBytes(StandardCharsets.UTF_8)); - ClusterHealthResponse healthResponse = client() - .filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue)) - .admin() - .cluster() - .prepareHealth() - .get(); - assertFalse(healthResponse.isTimedOut()); + // Assert that we can authenticate with the API KEY + final RestHighLevelClient restClient = new TestRestHighLevelClient(); + AuthenticateResponse authResponse = restClient.security().authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", + "ApiKey " + base64ApiKeyKeyValue).build()); + assertThat(authResponse.getUser().getUsername(), equalTo(SecuritySettingsSource.TEST_SUPERUSER)); // use the first ApiKey for an unauthorized action ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java index d56365a21a403..8900607efef55 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java @@ -55,7 +55,6 @@ import java.util.stream.Collectors; import static org.elasticsearch.test.SecuritySettingsSource.SECURITY_REQUEST_OPTIONS; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoTimeout; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -256,19 +255,20 @@ public void testRefreshingToken() throws IOException { CreateTokenResponse response = restClient.security().createToken(CreateTokenRequest.passwordGrant( SecuritySettingsSource.TEST_USER_NAME, SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()), SECURITY_REQUEST_OPTIONS); assertNotNull(response.getRefreshToken()); - // get cluster health with token - assertNoTimeout(client() - .filterWithHeader(Collections.singletonMap("Authorization", "Bearer " + response.getAccessToken())) - .admin().cluster().prepareHealth().get()); - + // Assert that we can authenticate with the access token + AuthenticateResponse authResponse = restClient.security().authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", + "Bearer " + response.getAccessToken()).build()); + assertThat(authResponse.getUser().getUsername(), equalTo(SecuritySettingsSource.TEST_USER_NAME)); CreateTokenResponse refreshResponse = restClient.security() .createToken(CreateTokenRequest.refreshTokenGrant(response.getRefreshToken()), SECURITY_REQUEST_OPTIONS); assertNotNull(refreshResponse.getRefreshToken()); assertNotEquals(refreshResponse.getRefreshToken(), response.getRefreshToken()); assertNotEquals(refreshResponse.getAccessToken(), response.getAccessToken()); - assertNoTimeout(client().filterWithHeader(Collections.singletonMap("Authorization", "Bearer " + refreshResponse.getAccessToken())) - .admin().cluster().prepareHealth().get()); + // Assert that we can authenticate with the refreshed access token + authResponse = restClient.security().authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", + "Bearer " + refreshResponse.getAccessToken()).build()); + assertThat(authResponse.getUser().getUsername(), equalTo(SecuritySettingsSource.TEST_USER_NAME)); } public void testRefreshingInvalidatedToken() throws IOException { @@ -466,10 +466,10 @@ public void testAuthenticateWithWrongToken() throws Exception { CreateTokenResponse response = restClient.security().createToken(CreateTokenRequest.passwordGrant( SecuritySettingsSource.TEST_USER_NAME, SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()), SECURITY_REQUEST_OPTIONS); assertNotNull(response.getRefreshToken()); - // First check that the correct access token works by getting cluster health with token - assertNoTimeout(client() - .filterWithHeader(Collections.singletonMap("Authorization", "Bearer " + response.getAccessToken())) - .admin().cluster().prepareHealth().get()); + // Assert that we can authenticate with the access token + AuthenticateResponse authResponse = restClient.security().authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", + "Bearer " + response.getAccessToken()).build()); + assertThat(authResponse.getUser().getUsername(), equalTo(SecuritySettingsSource.TEST_USER_NAME)); // Now attempt to authenticate with an invalid access token string RequestOptions wrongAuthOptions = RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + randomAlphaOfLengthBetween(0, 128)).build(); From 1c2b08347065d4a50591d3fe4b1c5e5b29e08930 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 13 Dec 2019 09:45:10 -0500 Subject: [PATCH 201/686] [ML] retry bulk indexing of state docs (#50149) This exchanges the direct use of the `Client` for `ResultsPersisterService`. State doc persistence will now retry. Failures to persist state will still not throw, but will be audited and logged. --- .../xpack/ml/MachineLearning.java | 14 ++++++--- .../NativeAnalyticsProcessFactory.java | 19 ++++++++---- .../NativeAutodetectProcessFactory.java | 19 ++++++++---- .../ml/process/IndexingStateProcessor.java | 28 ++++++++++++----- .../NativeAutodetectProcessFactoryTests.java | 15 +++++++--- .../process/IndexingStateProcessorTests.java | 30 ++++++++----------- 6 files changed, 80 insertions(+), 45 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java index a7b21bbd721ad..5216cf6a267fc 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java @@ -559,11 +559,17 @@ public Collection createComponents(Client client, ClusterService cluster environment, settings, nativeController, - client, - clusterService); + clusterService, + resultsPersisterService, + anomalyDetectionAuditor); normalizerProcessFactory = new NativeNormalizerProcessFactory(environment, nativeController, clusterService); - analyticsProcessFactory = new NativeAnalyticsProcessFactory(environment, client, nativeController, clusterService, - xContentRegistry); + analyticsProcessFactory = new NativeAnalyticsProcessFactory( + environment, + nativeController, + clusterService, + xContentRegistry, + resultsPersisterService, + dataFrameAnalyticsAuditor); memoryEstimationProcessFactory = new NativeMemoryUsageEstimationProcessFactory(environment, nativeController, clusterService); mlController = nativeController; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcessFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcessFactory.java index 1bd02add2f4ce..d2ace11f553ae 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcessFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcessFactory.java @@ -7,7 +7,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.client.Client; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesReference; @@ -20,10 +19,12 @@ import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import org.elasticsearch.xpack.ml.MachineLearning; import org.elasticsearch.xpack.ml.dataframe.process.results.AnalyticsResult; +import org.elasticsearch.xpack.ml.notifications.DataFrameAnalyticsAuditor; import org.elasticsearch.xpack.ml.process.IndexingStateProcessor; import org.elasticsearch.xpack.ml.process.NativeController; import org.elasticsearch.xpack.ml.process.ProcessPipes; import org.elasticsearch.xpack.ml.utils.NamedPipeHelper; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import java.io.IOException; import java.nio.file.Path; @@ -40,18 +41,24 @@ public class NativeAnalyticsProcessFactory implements AnalyticsProcessFactory resultsParser = new ProcessResultsParser<>(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY); NativeAutodetectProcess autodetect = new NativeAutodetectProcess( diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/IndexingStateProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/IndexingStateProcessor.java index 9bfd22500e02f..b3067dff993a7 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/IndexingStateProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/IndexingStateProcessor.java @@ -7,21 +7,22 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.action.bulk.BulkRequest; -import org.elasticsearch.client.Client; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.bytes.CompositeBytesReference; -import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.xpack.core.common.notifications.AbstractAuditMessage; +import org.elasticsearch.xpack.core.common.notifications.AbstractAuditor; import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; -import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; /** * Reads state documents of a stream, splits them and persists to an index via a bulk request @@ -32,12 +33,16 @@ public class IndexingStateProcessor implements StateProcessor { private static final int READ_BUF_SIZE = 8192; - private final Client client; private final String jobId; + private final AbstractAuditor auditor; + private final ResultsPersisterService resultsPersisterService; - public IndexingStateProcessor(Client client, String jobId) { - this.client = client; + public IndexingStateProcessor(String jobId, + ResultsPersisterService resultsPersisterService, + AbstractAuditor auditor) { this.jobId = jobId; + this.resultsPersisterService = resultsPersisterService; + this.auditor = auditor; } @Override @@ -98,8 +103,15 @@ void persist(BytesReference bytes) throws IOException { bulkRequest.add(bytes, AnomalyDetectorsIndex.jobStateIndexWriteAlias(), XContentType.JSON); if (bulkRequest.numberOfActions() > 0) { LOGGER.trace("[{}] Persisting job state document", jobId); - try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(ML_ORIGIN)) { - client.bulk(bulkRequest).actionGet(); + try { + resultsPersisterService.bulkIndexWithRetry(bulkRequest, + jobId, + () -> true, + (msg) -> auditor.warning(jobId, "Bulk indexing of state failed " + msg)); + } catch (Exception ex) { + String msg = "failed indexing updated state docs"; + LOGGER.error(() -> new ParameterizedMessage("[{}] {}", jobId, msg), ex); + auditor.error(jobId, msg + " error: " + ex.getMessage()); } } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessFactoryTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessFactoryTests.java index f486823be7b40..d20533fca15f2 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessFactoryTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessFactoryTests.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.ml.job.process.autodetect; -import org.elasticsearch.client.Client; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; @@ -16,8 +15,10 @@ import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.ml.MachineLearning; import org.elasticsearch.xpack.ml.job.process.autodetect.params.AutodetectParams; +import org.elasticsearch.xpack.ml.notifications.AnomalyDetectionAuditor; import org.elasticsearch.xpack.ml.process.NativeController; import org.elasticsearch.xpack.ml.process.ProcessPipes; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import java.io.IOException; import java.time.Duration; @@ -41,7 +42,8 @@ public void testSetProcessConnectTimeout() throws IOException { .build(); Environment env = TestEnvironment.newEnvironment(settings); NativeController nativeController = mock(NativeController.class); - Client client = mock(Client.class); + ResultsPersisterService resultsPersisterService = mock(ResultsPersisterService.class); + AnomalyDetectionAuditor anomalyDetectionAuditor = mock(AnomalyDetectionAuditor.class); ClusterSettings clusterSettings = new ClusterSettings(settings, Set.of(MachineLearning.PROCESS_CONNECT_TIMEOUT, AutodetectBuilder.MAX_ANOMALY_RECORDS_SETTING_DYNAMIC)); ClusterService clusterService = mock(ClusterService.class); @@ -51,8 +53,13 @@ public void testSetProcessConnectTimeout() throws IOException { AutodetectParams autodetectParams = mock(AutodetectParams.class); ProcessPipes processPipes = mock(ProcessPipes.class); - NativeAutodetectProcessFactory nativeAutodetectProcessFactory = - new NativeAutodetectProcessFactory(env, settings, nativeController, client, clusterService); + NativeAutodetectProcessFactory nativeAutodetectProcessFactory = new NativeAutodetectProcessFactory( + env, + settings, + nativeController, + clusterService, + resultsPersisterService, + anomalyDetectionAuditor); nativeAutodetectProcessFactory.setProcessConnectTimeout(TimeValue.timeValueSeconds(timeoutSeconds)); nativeAutodetectProcessFactory.createNativeProcess(job, autodetectParams, processPipes, Collections.emptyList()); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/process/IndexingStateProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/process/IndexingStateProcessorTests.java index f574782746c25..f65649c9ee1cc 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/process/IndexingStateProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/process/IndexingStateProcessorTests.java @@ -6,16 +6,16 @@ package org.elasticsearch.xpack.ml.process; import com.carrotsearch.randomizedtesting.annotations.Timeout; -import org.elasticsearch.action.ActionFuture; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; -import org.elasticsearch.client.Client; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.mock.orig.Mockito; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.ml.notifications.AnomalyDetectionAuditor; +import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import org.junit.After; import org.junit.Before; import org.mockito.ArgumentCaptor; @@ -54,24 +54,22 @@ public class IndexingStateProcessorTests extends ESTestCase { private static final int NUM_LARGE_DOCS = 2; private static final int LARGE_DOC_SIZE = 1000000; - private Client client; private IndexingStateProcessor stateProcessor; + private ResultsPersisterService resultsPersisterService; @Before - public void initialize() throws IOException { - client = mock(Client.class); - @SuppressWarnings("unchecked") - ActionFuture bulkResponseFuture = mock(ActionFuture.class); - stateProcessor = spy(new IndexingStateProcessor(client, JOB_ID)); - when(client.bulk(any(BulkRequest.class))).thenReturn(bulkResponseFuture); + public void initialize() { + resultsPersisterService = mock(ResultsPersisterService.class); + AnomalyDetectionAuditor auditor = mock(AnomalyDetectionAuditor.class); + stateProcessor = spy(new IndexingStateProcessor(JOB_ID, resultsPersisterService, auditor)); + when(resultsPersisterService.bulkIndexWithRetry(any(BulkRequest.class), any(), any(), any())).thenReturn(mock(BulkResponse.class)); ThreadPool threadPool = mock(ThreadPool.class); - when(client.threadPool()).thenReturn(threadPool); when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); } @After public void verifyNoMoreClientInteractions() { - Mockito.verifyNoMoreInteractions(client); + Mockito.verifyNoMoreInteractions(resultsPersisterService); } public void testStateRead() throws IOException { @@ -85,8 +83,7 @@ public void testStateRead() throws IOException { assertEquals(threeStates[0], capturedBytes.get(0).utf8ToString()); assertEquals(threeStates[1], capturedBytes.get(1).utf8ToString()); assertEquals(threeStates[2], capturedBytes.get(2).utf8ToString()); - verify(client, times(3)).bulk(any(BulkRequest.class)); - verify(client, times(3)).threadPool(); + verify(resultsPersisterService, times(3)).bulkIndexWithRetry(any(BulkRequest.class), any(), any(), any()); } public void testStateReadGivenConsecutiveZeroBytes() throws IOException { @@ -96,7 +93,7 @@ public void testStateReadGivenConsecutiveZeroBytes() throws IOException { stateProcessor.process(stream); verify(stateProcessor, never()).persist(any()); - Mockito.verifyNoMoreInteractions(client); + Mockito.verifyNoMoreInteractions(resultsPersisterService); } public void testStateReadGivenConsecutiveSpacesFollowedByZeroByte() throws IOException { @@ -106,7 +103,7 @@ public void testStateReadGivenConsecutiveSpacesFollowedByZeroByte() throws IOExc stateProcessor.process(stream); verify(stateProcessor, times(1)).persist(any()); - Mockito.verifyNoMoreInteractions(client); + Mockito.verifyNoMoreInteractions(resultsPersisterService); } /** @@ -128,7 +125,6 @@ public void testLargeStateRead() throws Exception { ByteArrayInputStream stream = new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8)); stateProcessor.process(stream); verify(stateProcessor, times(NUM_LARGE_DOCS)).persist(any()); - verify(client, times(NUM_LARGE_DOCS)).bulk(any(BulkRequest.class)); - verify(client, times(NUM_LARGE_DOCS)).threadPool(); + verify(resultsPersisterService, times(NUM_LARGE_DOCS)).bulkIndexWithRetry(any(BulkRequest.class), any(), any(), any()); } } From b8fe69ca3844766bcfe4ffaa1884b4f95599aa64 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Fri, 13 Dec 2019 09:16:53 -0700 Subject: [PATCH 202/686] Update remote cluster stats to support simple mode (#49961) Remote cluster stats API currently only returns useful information if the strategy in use is the SNIFF mode. This PR modifies the API to provide relevant information if the user is in the SIMPLE mode. This information is the configured addresses, max socket connections, and open socket connections. --- docs/reference/ccr/getting-started.asciidoc | 5 +- ...l => 15_connection_mode_configuration.yml} | 0 .../test/multi_cluster/20_info.yml | 47 +++++++- .../transport/RemoteClusterConnection.java | 20 +--- .../transport/RemoteConnectionInfo.java | 109 +++++++++--------- .../transport/RemoteConnectionStrategy.java | 28 ++++- .../transport/SimpleConnectionStrategy.java | 84 ++++++++++++++ .../transport/SniffConnectionStrategy.java | 81 +++++++++++++ .../RemoteClusterConnectionTests.java | 77 ++++++++----- .../RemoteConnectionStrategyTests.java | 5 + .../xpack/CcrSingleNodeTestCase.java | 2 +- .../xpack/ccr/RestartIndexFollowingIT.java | 3 +- 12 files changed, 344 insertions(+), 117 deletions(-) rename qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/{100_connection_mode_configuration.yml => 15_connection_mode_configuration.yml} (100%) diff --git a/docs/reference/ccr/getting-started.asciidoc b/docs/reference/ccr/getting-started.asciidoc index 41d013f62f3a7..0793051646cfe 100644 --- a/docs/reference/ccr/getting-started.asciidoc +++ b/docs/reference/ccr/getting-started.asciidoc @@ -135,7 +135,8 @@ remote cluster. "num_nodes_connected" : 1, <2> "max_connections_per_cluster" : 3, "initial_connect_timeout" : "30s", - "skip_unavailable" : false + "skip_unavailable" : false, + "mode" : "sniff" } } -------------------------------------------------- @@ -146,7 +147,7 @@ remote cluster. alias `leader` <2> This shows the number of nodes in the remote cluster the local cluster is connected to. - + Alternatively, you can manage remote clusters on the *Management / Elasticsearch / Remote Clusters* page in {kib}: diff --git a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/100_connection_mode_configuration.yml b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/15_connection_mode_configuration.yml similarity index 100% rename from qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/100_connection_mode_configuration.yml rename to qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/15_connection_mode_configuration.yml diff --git a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml index 59657e2012c8a..761526a7bea60 100644 --- a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml +++ b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml @@ -1,5 +1,5 @@ --- -"Fetch remote cluster info for existing cluster": +"Fetch remote cluster sniff info for existing cluster": - do: cluster.remote_info: {} @@ -7,6 +7,7 @@ - match: { my_remote_cluster.num_nodes_connected: 1} - match: { my_remote_cluster.max_connections_per_cluster: 1} - match: { my_remote_cluster.initial_connect_timeout: "30s" } + - match: { my_remote_cluster.mode: "sniff" } --- "Add transient remote cluster based on the preset cluster and check remote info": @@ -21,9 +22,13 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.seeds: $remote_ip + cluster.remote.test_remote_cluster.mode: "sniff" + cluster.remote.test_remote_cluster.sniff.node_connections: "2" + cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip - - match: {transient: {cluster.remote.test_remote_cluster.seeds: $remote_ip}} + - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "sniff"} + - match: {transient.cluster\.remote\.test_remote_cluster\.sniff\.node_connections: "2"} + - match: {transient.cluster\.remote\.test_remote_cluster\.sniff\.seeds: $remote_ip} # we do another search here since this will enforce the connection to be established # otherwise the cluster might not have been connected yet. @@ -45,19 +50,49 @@ - match: { my_remote_cluster.seeds.0: $remote_ip } - match: { my_remote_cluster.num_nodes_connected: 1} - - match: { test_remote_cluster.num_nodes_connected: 1} + - gt: { test_remote_cluster.num_nodes_connected: 0} - match: { my_remote_cluster.max_connections_per_cluster: 1} - - match: { test_remote_cluster.max_connections_per_cluster: 1} + - match: { test_remote_cluster.max_connections_per_cluster: 2} - match: { my_remote_cluster.initial_connect_timeout: "30s" } - match: { test_remote_cluster.initial_connect_timeout: "30s" } + - match: { my_remote_cluster.mode: "sniff" } + - match: { test_remote_cluster.mode: "sniff" } + + - do: + cluster.put_settings: + flat_settings: true + body: + transient: + cluster.remote.test_remote_cluster.mode: "simple" + cluster.remote.test_remote_cluster.sniff.seeds: null + cluster.remote.test_remote_cluster.sniff.node_connections: null + cluster.remote.test_remote_cluster.simple.socket_connections: "10" + cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + + - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "simple"} + - match: {transient.cluster\.remote\.test_remote_cluster\.simple\.socket_connections: "10"} + - match: {transient.cluster\.remote\.test_remote_cluster\.simple\.addresses: $remote_ip} + + - do: + cluster.remote_info: {} + + - match: { test_remote_cluster.connected: true } + - match: { test_remote_cluster.addresses.0: $remote_ip } + - gt: { test_remote_cluster.num_sockets_connected: 0} + - match: { test_remote_cluster.max_socket_connections: 10} + - match: { test_remote_cluster.initial_connect_timeout: "30s" } + - match: { test_remote_cluster.mode: "simple" } + - do: cluster.put_settings: body: transient: - cluster.remote.test_remote_cluster.seeds: null + cluster.remote.test_remote_cluster.mode: null + cluster.remote.test_remote_cluster.simple.socket_connections: null + cluster.remote.test_remote_cluster.simple.addresses: null --- "skip_unavailable is returned as part of _remote/info response": diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java index 8b89e8f8f8905..ae697bba95b49 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java @@ -34,7 +34,6 @@ import java.io.Closeable; import java.io.IOException; -import java.util.Collections; import java.util.function.Function; /** @@ -206,24 +205,7 @@ boolean isNodeConnected(final DiscoveryNode node) { * Get the information about remote nodes to be rendered on {@code _remote/info} requests. */ public RemoteConnectionInfo getConnectionInfo() { - if (connectionStrategy instanceof SniffConnectionStrategy) { - SniffConnectionStrategy sniffStrategy = (SniffConnectionStrategy) this.connectionStrategy; - return new RemoteConnectionInfo( - clusterAlias, - sniffStrategy.getSeedNodes(), - sniffStrategy.getMaxConnections(), - getNumNodesConnected(), - initialConnectionTimeout, - skipUnavailable); - } else { - return new RemoteConnectionInfo( - clusterAlias, - Collections.emptyList(), - 0, - getNumNodesConnected(), - initialConnectionTimeout, - skipUnavailable); - } + return new RemoteConnectionInfo(clusterAlias, connectionStrategy.getModeInfo(), initialConnectionTimeout, skipUnavailable); } int getNumNodesConnected() { diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java index 5bdc6f9874330..e721a0b617fd1 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java @@ -19,6 +19,7 @@ package org.elasticsearch.transport; +import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -36,63 +37,67 @@ * {@code _remote/info} requests. */ public final class RemoteConnectionInfo implements ToXContentFragment, Writeable { - final List seedNodes; - final int connectionsPerCluster; + + final ModeInfo modeInfo; final TimeValue initialConnectionTimeout; - final int numNodesConnected; final String clusterAlias; final boolean skipUnavailable; - RemoteConnectionInfo(String clusterAlias, List seedNodes, - int connectionsPerCluster, int numNodesConnected, - TimeValue initialConnectionTimeout, boolean skipUnavailable) { + RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, TimeValue initialConnectionTimeout, boolean skipUnavailable) { this.clusterAlias = clusterAlias; - this.seedNodes = seedNodes; - this.connectionsPerCluster = connectionsPerCluster; - this.numNodesConnected = numNodesConnected; + this.modeInfo = modeInfo; this.initialConnectionTimeout = initialConnectionTimeout; this.skipUnavailable = skipUnavailable; } public RemoteConnectionInfo(StreamInput input) throws IOException { - seedNodes = Arrays.asList(input.readStringArray()); - connectionsPerCluster = input.readVInt(); - initialConnectionTimeout = input.readTimeValue(); - numNodesConnected = input.readVInt(); - clusterAlias = input.readString(); - skipUnavailable = input.readBoolean(); - } - - public List getSeedNodes() { - return seedNodes; - } - - public int getConnectionsPerCluster() { - return connectionsPerCluster; - } - - public TimeValue getInitialConnectionTimeout() { - return initialConnectionTimeout; + // TODO: Change to 7.6 after backport + if (input.getVersion().onOrAfter(Version.V_8_0_0)) { + RemoteConnectionStrategy.ConnectionStrategy mode = input.readEnum(RemoteConnectionStrategy.ConnectionStrategy.class); + modeInfo = mode.getReader().read(input); + initialConnectionTimeout = input.readTimeValue(); + clusterAlias = input.readString(); + skipUnavailable = input.readBoolean(); + } else { + List seedNodes = Arrays.asList(input.readStringArray()); + int connectionsPerCluster = input.readVInt(); + initialConnectionTimeout = input.readTimeValue(); + int numNodesConnected = input.readVInt(); + clusterAlias = input.readString(); + skipUnavailable = input.readBoolean(); + modeInfo = new SniffConnectionStrategy.SniffModeInfo(seedNodes, connectionsPerCluster, numNodesConnected); + } } - public int getNumNodesConnected() { - return numNodesConnected; + public boolean isConnected() { + return modeInfo.isConnected(); } public String getClusterAlias() { return clusterAlias; } - public boolean isSkipUnavailable() { - return skipUnavailable; - } - @Override public void writeTo(StreamOutput out) throws IOException { - out.writeStringArray(seedNodes.toArray(new String[0])); - out.writeVInt(connectionsPerCluster); - out.writeTimeValue(initialConnectionTimeout); - out.writeVInt(numNodesConnected); + // TODO: Change to 7.6 after backport + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeEnum(modeInfo.modeType()); + modeInfo.writeTo(out); + out.writeTimeValue(initialConnectionTimeout); + } else { + if (modeInfo.modeType() == RemoteConnectionStrategy.ConnectionStrategy.SNIFF) { + SniffConnectionStrategy.SniffModeInfo sniffInfo = (SniffConnectionStrategy.SniffModeInfo) this.modeInfo; + out.writeStringArray(sniffInfo.seedNodes.toArray(new String[0])); + out.writeVInt(sniffInfo.maxConnectionsPerCluster); + out.writeTimeValue(initialConnectionTimeout); + out.writeVInt(sniffInfo.numNodesConnected); + } else { + out.writeStringArray(new String[0]); + out.writeVInt(0); + out.writeTimeValue(initialConnectionTimeout); + out.writeVInt(0); + } + } out.writeString(clusterAlias); out.writeBoolean(skipUnavailable); } @@ -101,14 +106,9 @@ public void writeTo(StreamOutput out) throws IOException { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(clusterAlias); { - builder.startArray("seeds"); - for (String addr : seedNodes) { - builder.value(addr); - } - builder.endArray(); - builder.field("connected", numNodesConnected > 0); - builder.field("num_nodes_connected", numNodesConnected); - builder.field("max_connections_per_cluster", connectionsPerCluster); + builder.field("connected", modeInfo.isConnected()); + builder.field("mode", modeInfo.modeName()); + modeInfo.toXContent(builder, params); builder.field("initial_connect_timeout", initialConnectionTimeout); builder.field("skip_unavailable", skipUnavailable); } @@ -121,18 +121,23 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RemoteConnectionInfo that = (RemoteConnectionInfo) o; - return connectionsPerCluster == that.connectionsPerCluster && - numNodesConnected == that.numNodesConnected && - Objects.equals(seedNodes, that.seedNodes) && + return skipUnavailable == that.skipUnavailable && + Objects.equals(modeInfo, that.modeInfo) && Objects.equals(initialConnectionTimeout, that.initialConnectionTimeout) && - Objects.equals(clusterAlias, that.clusterAlias) && - skipUnavailable == that.skipUnavailable; + Objects.equals(clusterAlias, that.clusterAlias); } @Override public int hashCode() { - return Objects.hash(seedNodes, connectionsPerCluster, initialConnectionTimeout, - numNodesConnected, clusterAlias, skipUnavailable); + return Objects.hash(modeInfo, initialConnectionTimeout, clusterAlias, skipUnavailable); } + public interface ModeInfo extends ToXContentFragment, Writeable { + + boolean isConnected(); + + String modeName(); + + RemoteConnectionStrategy.ConnectionStrategy modeType(); + } } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java index d8a459a79a56e..3d994f35de224 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java @@ -27,6 +27,7 @@ import org.elasticsearch.action.support.ContextPreservingActionListener; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -57,20 +58,39 @@ public abstract class RemoteConnectionStrategy implements TransportConnectionListener, Closeable { enum ConnectionStrategy { - SNIFF(SniffConnectionStrategy.CHANNELS_PER_CONNECTION, SniffConnectionStrategy::enablementSettings), - SIMPLE(SimpleConnectionStrategy.CHANNELS_PER_CONNECTION, SimpleConnectionStrategy::enablementSettings); + SNIFF(SniffConnectionStrategy.CHANNELS_PER_CONNECTION, SniffConnectionStrategy::enablementSettings, + SniffConnectionStrategy::infoReader) { + @Override + public String toString() { + return "sniff"; + } + }, + SIMPLE(SimpleConnectionStrategy.CHANNELS_PER_CONNECTION, SimpleConnectionStrategy::enablementSettings, + SimpleConnectionStrategy::infoReader) { + @Override + public String toString() { + return "simple"; + } + }; private final int numberOfChannels; private final Supplier>> enablementSettings; + private final Supplier> reader; - ConnectionStrategy(int numberOfChannels, Supplier>> enablementSettings) { + ConnectionStrategy(int numberOfChannels, Supplier>> enablementSettings, + Supplier> reader) { this.numberOfChannels = numberOfChannels; this.enablementSettings = enablementSettings; + this.reader = reader; } public int getNumberOfChannels() { return numberOfChannels; } + + public Writeable.Reader getReader() { + return reader.get(); + } } public static final Setting.AffixSetting REMOTE_CONNECTION_MODE = Setting.affixKeySetting( @@ -310,6 +330,8 @@ boolean assertNoRunningConnections() { protected abstract void connectImpl(ActionListener listener); + protected abstract RemoteConnectionInfo.ModeInfo getModeInfo(); + private List> getAndClearListeners() { final List> result; synchronized (mutex) { diff --git a/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java index 890cdaf25387b..0250ff73e4e8c 100644 --- a/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java @@ -26,14 +26,21 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.concurrent.CountDown; +import org.elasticsearch.common.xcontent.XContentBuilder; +import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -131,6 +138,10 @@ static Stream> enablementSettings() { return Stream.of(SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES); } + static Writeable.Reader infoReader() { + return SimpleModeInfo::new; + } + @Override protected boolean shouldOpenMoreConnections() { return connectionManager.size() < maxNumConnections; @@ -153,6 +164,11 @@ protected void connectImpl(ActionListener listener) { performSimpleConnectionProcess(listener); } + @Override + public RemoteConnectionInfo.ModeInfo getModeInfo() { + return new SimpleModeInfo(configuredAddresses, maxNumConnections, connectionManager.size()); + } + private void performSimpleConnectionProcess(ActionListener listener) { openConnections(listener, 1); } @@ -238,4 +254,72 @@ private boolean addressesChanged(final List oldAddresses, final List newSeeds = new HashSet<>(newAddresses); return oldSeeds.equals(newSeeds) == false; } + + static class SimpleModeInfo implements RemoteConnectionInfo.ModeInfo { + + private final List addresses; + private final int maxSocketConnections; + private final int numSocketsConnected; + + SimpleModeInfo(List addresses, int maxSocketConnections, int numSocketsConnected) { + this.addresses = addresses; + this.maxSocketConnections = maxSocketConnections; + this.numSocketsConnected = numSocketsConnected; + } + + private SimpleModeInfo(StreamInput input) throws IOException { + addresses = Arrays.asList(input.readStringArray()); + maxSocketConnections = input.readVInt(); + numSocketsConnected = input.readVInt(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startArray("addresses"); + for (String address : addresses) { + builder.value(address); + } + builder.endArray(); + builder.field("num_sockets_connected", numSocketsConnected); + builder.field("max_socket_connections", maxSocketConnections); + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeStringArray(addresses.toArray(new String[0])); + out.writeVInt(maxSocketConnections); + out.writeVInt(numSocketsConnected); + } + + @Override + public boolean isConnected() { + return numSocketsConnected > 0; + } + + @Override + public String modeName() { + return "simple"; + } + + @Override + public RemoteConnectionStrategy.ConnectionStrategy modeType() { + return RemoteConnectionStrategy.ConnectionStrategy.SIMPLE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SimpleModeInfo simple = (SimpleModeInfo) o; + return maxSocketConnections == simple.maxSocketConnections && + numSocketsConnected == simple.numSocketsConnected && + Objects.equals(addresses, simple.addresses); + } + + @Override + public int hashCode() { + return Objects.hash(addresses, maxSocketConnections, numSocketsConnected); + } + } } diff --git a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java index cfed1d01c47e3..ad1dc6696b57c 100644 --- a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java @@ -36,15 +36,19 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; import java.net.InetSocketAddress; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; @@ -193,6 +197,10 @@ static Stream> enablementSettings() { return Stream.of(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS, SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD); } + static Writeable.Reader infoReader() { + return SniffModeInfo::new; + } + @Override protected boolean shouldOpenMoreConnections() { return connectionManager.size() < maxNumRemoteConnections; @@ -217,6 +225,11 @@ protected void connectImpl(ActionListener listener) { collectRemoteNodes(seedNodes.iterator(), listener); } + @Override + protected RemoteConnectionInfo.ModeInfo getModeInfo() { + return new SniffModeInfo(configuredSeedNodes, maxNumRemoteConnections, connectionManager.size()); + } + private void collectRemoteNodes(Iterator> seedNodes, ActionListener listener) { if (Thread.currentThread().isInterrupted()) { listener.onFailure(new InterruptedException("remote connect thread got interrupted")); @@ -469,4 +482,72 @@ private boolean proxyChanged(String oldProxy, String newProxy) { return Objects.equals(oldProxy, newProxy) == false; } + + static class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { + + final List seedNodes; + final int maxConnectionsPerCluster; + final int numNodesConnected; + + SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { + this.seedNodes = seedNodes; + this.maxConnectionsPerCluster = maxConnectionsPerCluster; + this.numNodesConnected = numNodesConnected; + } + + private SniffModeInfo(StreamInput input) throws IOException { + seedNodes = Arrays.asList(input.readStringArray()); + maxConnectionsPerCluster = input.readVInt(); + numNodesConnected = input.readVInt(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startArray("seeds"); + for (String address : seedNodes) { + builder.value(address); + } + builder.endArray(); + builder.field("num_nodes_connected", numNodesConnected); + builder.field("max_connections_per_cluster", maxConnectionsPerCluster); + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeStringArray(seedNodes.toArray(new String[0])); + out.writeVInt(maxConnectionsPerCluster); + out.writeVInt(numNodesConnected); + } + + @Override + public boolean isConnected() { + return numNodesConnected > 0; + } + + @Override + public String modeName() { + return "sniff"; + } + + @Override + public RemoteConnectionStrategy.ConnectionStrategy modeType() { + return RemoteConnectionStrategy.ConnectionStrategy.SNIFF; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SniffModeInfo sniff = (SniffModeInfo) o; + return maxConnectionsPerCluster == sniff.maxConnectionsPerCluster && + numNodesConnected == sniff.numNodesConnected && + Objects.equals(seedNodes, sniff.seedNodes); + } + + @Override + public int hashCode() { + return Objects.hash(seedNodes, maxConnectionsPerCluster, numNodesConnected); + } + } } diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java index d74a8daa98d61..934cf81a503ea 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java @@ -324,7 +324,8 @@ public void testGetConnectionInfo() throws Exception { List seedNodes = addresses(node3, node1, node2); Collections.shuffle(seedNodes, random()); - try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { + try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, + threadPool, null)) { service.start(); service.acceptIncomingRequests(); int maxNumConnections = randomIntBetween(1, 5); @@ -335,9 +336,10 @@ public void testGetConnectionInfo() throws Exception { // test no nodes connected RemoteConnectionInfo remoteConnectionInfo = assertSerialization(connection.getConnectionInfo()); assertNotNull(remoteConnectionInfo); - assertEquals(0, remoteConnectionInfo.numNodesConnected); - assertEquals(3, remoteConnectionInfo.seedNodes.size()); - assertEquals(maxNumConnections, remoteConnectionInfo.connectionsPerCluster); + SniffConnectionStrategy.SniffModeInfo sniffInfo = (SniffConnectionStrategy.SniffModeInfo) remoteConnectionInfo.modeInfo; + assertEquals(0, sniffInfo.numNodesConnected); + assertEquals(3, sniffInfo.seedNodes.size()); + assertEquals(maxNumConnections, sniffInfo.maxConnectionsPerCluster); assertEquals(clusterAlias, remoteConnectionInfo.clusterAlias); } } @@ -345,32 +347,37 @@ public void testGetConnectionInfo() throws Exception { } public void testRemoteConnectionInfo() throws IOException { + List remoteAddresses = Collections.singletonList("seed:1"); + + RemoteConnectionInfo.ModeInfo modeInfo1; + RemoteConnectionInfo.ModeInfo modeInfo2; + + if (randomBoolean()) { + modeInfo1 = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 4, 4); + modeInfo2 = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 4, 3); + } else { + modeInfo1 = new SimpleConnectionStrategy.SimpleModeInfo(remoteAddresses, 18, 18); + modeInfo2 = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 18, 17); + } + RemoteConnectionInfo stats = - new RemoteConnectionInfo("test_cluster", Arrays.asList("seed:1"), 4, 3, TimeValue.timeValueMinutes(30), false); + new RemoteConnectionInfo("test_cluster", modeInfo1, TimeValue.timeValueMinutes(30), false); assertSerialization(stats); RemoteConnectionInfo stats1 = - new RemoteConnectionInfo("test_cluster", Arrays.asList("seed:1"), 4, 4, TimeValue.timeValueMinutes(30), true); - assertSerialization(stats1); - assertNotEquals(stats, stats1); - - stats1 = new RemoteConnectionInfo("test_cluster_1", Arrays.asList("seed:1"), 4, 3, TimeValue.timeValueMinutes(30), false); - assertSerialization(stats1); - assertNotEquals(stats, stats1); - - stats1 = new RemoteConnectionInfo("test_cluster", Arrays.asList("seed:15"), 4, 3, TimeValue.timeValueMinutes(30), false); + new RemoteConnectionInfo("test_cluster", modeInfo1, TimeValue.timeValueMinutes(30), true); assertSerialization(stats1); assertNotEquals(stats, stats1); - stats1 = new RemoteConnectionInfo("test_cluster", Arrays.asList("seed:1"), 4, 3, TimeValue.timeValueMinutes(30), true); + stats1 = new RemoteConnectionInfo("test_cluster_1", modeInfo1, TimeValue.timeValueMinutes(30), false); assertSerialization(stats1); assertNotEquals(stats, stats1); - stats1 = new RemoteConnectionInfo("test_cluster", Arrays.asList("seed:1"), 4, 3, TimeValue.timeValueMinutes(325), true); + stats1 = new RemoteConnectionInfo("test_cluster", modeInfo1, TimeValue.timeValueMinutes(325), false); assertSerialization(stats1); assertNotEquals(stats, stats1); - stats1 = new RemoteConnectionInfo("test_cluster", Arrays.asList("seed:1"), 5, 3, TimeValue.timeValueMinutes(30), false); + stats1 = new RemoteConnectionInfo("test_cluster", modeInfo2, TimeValue.timeValueMinutes(30), false); assertSerialization(stats1); assertNotEquals(stats, stats1); } @@ -389,27 +396,33 @@ private static RemoteConnectionInfo assertSerialization(RemoteConnectionInfo inf } public void testRenderConnectionInfoXContent() throws IOException { - RemoteConnectionInfo stats = - new RemoteConnectionInfo("test_cluster", Arrays.asList("seed:1"), 4, 3, TimeValue.timeValueMinutes(30), true); + List remoteAddresses = Arrays.asList("seed:1", "seed:2"); + + RemoteConnectionInfo.ModeInfo modeInfo; + + boolean sniff = randomBoolean(); + if (sniff) { + modeInfo = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 3, 2); + } else { + modeInfo = new SimpleConnectionStrategy.SimpleModeInfo(remoteAddresses, 18, 16); + } + + RemoteConnectionInfo stats = new RemoteConnectionInfo("test_cluster", modeInfo, TimeValue.timeValueMinutes(30), true); stats = assertSerialization(stats); XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject(); stats.toXContent(builder, null); builder.endObject(); - assertEquals("{\"test_cluster\":{\"seeds\":[\"seed:1\"],\"connected\":true," + - "\"num_nodes_connected\":3,\"max_connections_per_cluster\":4,\"initial_connect_timeout\":\"30m\"," + - "\"skip_unavailable\":true}}", Strings.toString(builder)); - stats = new RemoteConnectionInfo( - "some_other_cluster", Arrays.asList("seed:1", "seed:2"), 2, 0, TimeValue.timeValueSeconds(30), false); - stats = assertSerialization(stats); - builder = XContentFactory.jsonBuilder(); - builder.startObject(); - stats.toXContent(builder, null); - builder.endObject(); - assertEquals("{\"some_other_cluster\":{\"seeds\":[\"seed:1\",\"seed:2\"]," - + "\"connected\":false,\"num_nodes_connected\":0,\"max_connections_per_cluster\":2,\"initial_connect_timeout\":\"30s\"," + - "\"skip_unavailable\":false}}", Strings.toString(builder)); + if (sniff) { + assertEquals("{\"test_cluster\":{\"connected\":true,\"mode\":\"sniff\",\"seeds\":[\"seed:1\",\"seed:2\"]," + + "\"num_nodes_connected\":2,\"max_connections_per_cluster\":3,\"initial_connect_timeout\":\"30m\"," + + "\"skip_unavailable\":true}}", Strings.toString(builder)); + } else { + assertEquals("{\"test_cluster\":{\"connected\":true,\"mode\":\"simple\",\"addresses\":[\"seed:1\",\"seed:2\"]," + + "\"num_sockets_connected\":16,\"max_socket_connections\":18,\"initial_connect_timeout\":\"30m\"," + + "\"skip_unavailable\":true}}", Strings.toString(builder)); + } } public void testCollectNodes() throws Exception { diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteConnectionStrategyTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteConnectionStrategyTests.java index 5ea54c7356b94..2c6fc691ec5bd 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteConnectionStrategyTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteConnectionStrategyTests.java @@ -111,5 +111,10 @@ protected boolean shouldOpenMoreConnections() { protected void connectImpl(ActionListener listener) { } + + @Override + protected RemoteConnectionInfo.ModeInfo getModeInfo() { + return null; + } } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrSingleNodeTestCase.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrSingleNodeTestCase.java index 3a07ef9aa8852..cbfd078452368 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrSingleNodeTestCase.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrSingleNodeTestCase.java @@ -69,7 +69,7 @@ public void setupLocalRemote() throws Exception { List infos = client().execute(RemoteInfoAction.INSTANCE, new RemoteInfoRequest()).get().getInfos(); assertThat(infos.size(), equalTo(1)); - assertThat(infos.get(0).getNumNodesConnected(), equalTo(1)); + assertTrue(infos.get(0).isConnected()); } @Before diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/RestartIndexFollowingIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/RestartIndexFollowingIT.java index ed3d1abb35770..88370aed0db71 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/RestartIndexFollowingIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/RestartIndexFollowingIT.java @@ -23,7 +23,6 @@ import static java.util.Collections.singletonMap; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; public class RestartIndexFollowingIT extends CcrIntegTestCase { @@ -96,7 +95,7 @@ private void setupRemoteCluster() throws Exception { List infos = followerClient().execute(RemoteInfoAction.INSTANCE, new RemoteInfoRequest()).get().getInfos(); assertThat(infos.size(), equalTo(1)); - assertThat(infos.get(0).getNumNodesConnected(), greaterThanOrEqualTo(1)); + assertTrue(infos.get(0).isConnected()); } private void cleanRemoteCluster() throws Exception { From 0271d2847b965d620e1209377deb829d4f223c8b Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Fri, 13 Dec 2019 12:44:12 -0500 Subject: [PATCH 203/686] [DOCS] Add `index-extra-title-page.html` for direct HTML migration (#50189) --- docs/reference/index-extra-title-page.html | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/reference/index-extra-title-page.html diff --git a/docs/reference/index-extra-title-page.html b/docs/reference/index-extra-title-page.html new file mode 100644 index 0000000000000..69eff59c9e9bd --- /dev/null +++ b/docs/reference/index-extra-title-page.html @@ -0,0 +1,20 @@ +
    +

    + Welcome to the official documentation for Elasticsearch: + the search and analytics engine that powers the Elastic Stack. + If you want to learn how to use Elasticsearch to search and analyze your + data, you've come to the right place. This guide shows you how to: +

    +
    +
      +
    • Install, configure, and administer an Elasticsearch + cluster.
    • +
    • Index your data, optimize your indices, and search + with the Elasticsearch query language. +
    • +
    • Discover trends, patterns, and anomalies with + aggregations and the machine learning APIs. +
    • +
    +
    +
    \ No newline at end of file From 26cdf0ad736833106e5d819998b5396eaceb2809 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 13 Dec 2019 13:56:50 -0500 Subject: [PATCH 204/686] Migrate peer recovery from translog to retention lease (#49448) Since 7.4, we switch from translog to Lucene as the source of history for peer recoveries. However, we reduce the likelihood of operation-based recoveries when performing a full cluster restart from pre-7.4 because existing copies do not have PPRL. To remedy this issue, we fallback using translog in peer recoveries if the recovering replica does not have a peer recovery retention lease, and the replication group hasn't fully migrated to PRRL. Relates #45136 --- .../upgrades/FullClusterRestartIT.java | 66 ++++++++++++++ .../elasticsearch/upgrades/RecoveryIT.java | 57 +++++++++++- .../elasticsearch/index/IndexSettings.java | 24 +++-- .../elasticsearch/index/engine/Engine.java | 24 +++-- .../index/engine/InternalEngine.java | 88 +++++++++++-------- .../index/engine/ReadOnlyEngine.java | 13 +-- .../index/seqno/ReplicationTracker.java | 9 +- .../elasticsearch/index/shard/IndexShard.java | 70 ++++++++++++--- .../index/shard/PrimaryReplicaSyncer.java | 5 +- .../recovery/RecoverySourceHandler.java | 37 ++++---- .../indices/recovery/RecoveryTarget.java | 4 +- .../index/IndexSettingsTests.java | 2 +- .../index/engine/InternalEngineTests.java | 30 ++++--- .../IndexLevelReplicationTests.java | 9 +- .../RetentionLeasesReplicationTests.java | 22 +++++ .../index/seqno/RetentionLeaseIT.java | 7 +- .../shard/PrimaryReplicaSyncerTests.java | 6 +- .../indices/recovery/RecoveryTests.java | 2 +- .../test/rest/ESRestTestCase.java | 75 ++++++++++++++++ .../action/bulk/BulkShardOperationsTests.java | 3 +- 20 files changed, 439 insertions(+), 114 deletions(-) diff --git a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java index ec961534eb0af..2d367261f8895 100644 --- a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java +++ b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.seqno.RetentionLeaseUtils; import org.elasticsearch.test.NotEqualMessageBuilder; import org.elasticsearch.test.rest.ESRestTestCase; @@ -1168,6 +1169,12 @@ private void indexRandomDocuments( } } + private void indexDocument(String id) throws IOException { + final Request indexRequest = new Request("POST", "/" + index + "/" + "_doc/" + id); + indexRequest.setJsonEntity(Strings.toString(JsonXContent.contentBuilder().startObject().field("f", "v").endObject())); + assertOK(client().performRequest(indexRequest)); + } + private int countOfIndexedRandomDocuments() throws IOException { return Integer.parseInt(loadInfoDocument(index + "_count")); } @@ -1248,4 +1255,63 @@ public void testPeerRecoveryRetentionLeases() throws IOException { RetentionLeaseUtils.assertAllCopiesHavePeerRecoveryRetentionLeases(client(), index); } } + + /** + * Tests that with or without soft-deletes, we should perform an operation-based recovery if there were some + * but not too many uncommitted documents (i.e., less than 10% of committed documents or the extra translog) + * before we restart the cluster. This is important when we move from translog based to retention leases based + * peer recoveries. + */ + public void testOperationBasedRecovery() throws Exception { + if (isRunningAgainstOldCluster()) { + createIndex(index, Settings.builder() + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) + .build()); + ensureGreen(index); + int committedDocs = randomIntBetween(100, 200); + for (int i = 0; i < committedDocs; i++) { + indexDocument(Integer.toString(i)); + if (rarely()) { + flush(index, randomBoolean()); + } + } + flush(index, true); + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index); + // less than 10% of the committed docs (see IndexSetting#FILE_BASED_RECOVERY_THRESHOLD_SETTING). + int uncommittedDocs = randomIntBetween(0, (int) (committedDocs * 0.1)); + for (int i = 0; i < uncommittedDocs; i++) { + final String id = Integer.toString(randomIntBetween(1, 100)); + indexDocument(id); + } + } else { + ensureGreen(index); + assertNoFileBasedRecovery(index, n -> true); + } + } + + /** + * Verifies that once all shard copies on the new version, we should turn off the translog retention for indices with soft-deletes. + */ + public void testTurnOffTranslogRetentionAfterUpgraded() throws Exception { + if (isRunningAgainstOldCluster()) { + createIndex(index, Settings.builder() + .put(IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1) + .put(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 1) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true).build()); + ensureGreen(index); + int numDocs = randomIntBetween(10, 100); + for (int i = 0; i < numDocs; i++) { + indexDocument(Integer.toString(randomIntBetween(1, 100))); + if (rarely()) { + flush(index, randomBoolean()); + } + } + } else { + ensureGreen(index); + flush(index, true); + assertEmptyTranslog(index); + } + } } diff --git a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java index 58bff342085ee..cd4a07aab3ec0 100644 --- a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java +++ b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java @@ -487,10 +487,10 @@ public void testClosedIndexNoopRecovery() throws Exception { switch (CLUSTER_TYPE) { case OLD: break; case MIXED: - assertNoFileBasedRecovery(indexName, s -> s.startsWith(CLUSTER_NAME + "-0")); + assertNoopRecoveries(indexName, s -> s.startsWith(CLUSTER_NAME + "-0")); break; case UPGRADED: - assertNoFileBasedRecovery(indexName, s -> s.startsWith(CLUSTER_NAME)); + assertNoopRecoveries(indexName, s -> s.startsWith(CLUSTER_NAME)); break; } } @@ -647,7 +647,7 @@ public void testUpdateDoc() throws Exception { } } - private void assertNoFileBasedRecovery(String indexName, Predicate targetNode) throws IOException { + private void assertNoopRecoveries(String indexName, Predicate targetNode) throws IOException { Map recoveries = entityAsMap(client() .performRequest(new Request("GET", indexName + "/_recovery?detailed=true"))); @@ -678,4 +678,55 @@ private void assertNoFileBasedRecovery(String indexName, Predicate targe assertTrue("must find replica", foundReplica); } + + /** + * Tests that with or without soft-deletes, we should perform an operation-based recovery if there were some + * but not too many uncommitted documents (i.e., less than 10% of committed documents or the extra translog) + * before we upgrade each node. This is important when we move from translog based to retention leases based + * peer recoveries. + */ + public void testOperationBasedRecovery() throws Exception { + final String index = "test_operation_based_recovery"; + if (CLUSTER_TYPE == ClusterType.OLD) { + createIndex(index, Settings.builder() + .put(IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1) + .put(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 2) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()).build()); + ensureGreen(index); + indexDocs(index, 0, randomIntBetween(100, 200)); + flush(index, randomBoolean()); + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index); + // uncommitted docs must be less than 10% of committed docs (see IndexSetting#FILE_BASED_RECOVERY_THRESHOLD_SETTING). + indexDocs(index, randomIntBetween(0, 100), randomIntBetween(0, 3)); + } else { + ensureGreen(index); + assertNoFileBasedRecovery(index, nodeName -> + CLUSTER_TYPE == ClusterType.UPGRADED + || nodeName.startsWith(CLUSTER_NAME + "-0") + || (nodeName.startsWith(CLUSTER_NAME + "-1") && Booleans.parseBoolean(System.getProperty("tests.first_round")) == false)); + indexDocs(index, randomIntBetween(0, 100), randomIntBetween(0, 3)); + } + } + + /** + * Verifies that once all shard copies on the new version, we should turn off the translog retention for indices with soft-deletes. + */ + public void testTurnOffTranslogRetentionAfterUpgraded() throws Exception { + final String index = "turn_off_translog_retention"; + if (CLUSTER_TYPE == ClusterType.OLD) { + createIndex(index, Settings.builder() + .put(IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1) + .put(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), randomIntBetween(0, 2)) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true).build()); + ensureGreen(index); + indexDocs(index, 0, randomIntBetween(100, 200)); + flush(index, randomBoolean()); + indexDocs(index, randomIntBetween(0, 100), randomIntBetween(0, 100)); + } + if (CLUSTER_TYPE == ClusterType.UPGRADED) { + ensureGreen(index); + flush(index, true); + assertEmptyTranslog(index); + } + } } diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index a1994d65c8d73..99076f812b96e 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -245,21 +245,22 @@ public final class IndexSettings { * Controls how long translog files that are no longer needed for persistence reasons * will be kept around before being deleted. Keeping more files is useful to increase * the chance of ops based recoveries for indices with soft-deletes disabled. - * This setting will be ignored if soft-deletes is enabled. + * This setting will be ignored if soft-deletes is used in peer recoveries (default in 7.4). **/ public static final Setting INDEX_TRANSLOG_RETENTION_AGE_SETTING = Setting.timeSetting("index.translog.retention.age", - settings -> INDEX_SOFT_DELETES_SETTING.get(settings) ? TimeValue.MINUS_ONE : TimeValue.timeValueHours(12), TimeValue.MINUS_ONE, - Property.Dynamic, Property.IndexScope); + settings -> shouldDisableTranslogRetention(settings) ? TimeValue.MINUS_ONE : TimeValue.timeValueHours(12), + TimeValue.MINUS_ONE, Property.Dynamic, Property.IndexScope); /** * Controls how many translog files that are no longer needed for persistence reasons * will be kept around before being deleted. Keeping more files is useful to increase * the chance of ops based recoveries for indices with soft-deletes disabled. - * This setting will be ignored if soft-deletes is enabled. + * This setting will be ignored if soft-deletes is used in peer recoveries (default in 7.4). **/ public static final Setting INDEX_TRANSLOG_RETENTION_SIZE_SETTING = - Setting.byteSizeSetting("index.translog.retention.size", settings -> INDEX_SOFT_DELETES_SETTING.get(settings) ? "-1" : "512MB", + Setting.byteSizeSetting("index.translog.retention.size", + settings -> shouldDisableTranslogRetention(settings) ? "-1" : "512MB", Property.Dynamic, Property.IndexScope); /** @@ -577,7 +578,7 @@ private void setFlushAfterMergeThresholdSize(ByteSizeValue byteSizeValue) { } private void setTranslogRetentionSize(ByteSizeValue byteSizeValue) { - if (softDeleteEnabled && byteSizeValue.getBytes() >= 0) { + if (shouldDisableTranslogRetention(settings) && byteSizeValue.getBytes() >= 0) { // ignore the translog retention settings if soft-deletes enabled this.translogRetentionSize = new ByteSizeValue(-1); } else { @@ -586,7 +587,7 @@ private void setTranslogRetentionSize(ByteSizeValue byteSizeValue) { } private void setTranslogRetentionAge(TimeValue age) { - if (softDeleteEnabled && age.millis() >= 0) { + if (shouldDisableTranslogRetention(settings) && age.millis() >= 0) { // ignore the translog retention settings if soft-deletes enabled this.translogRetentionAge = TimeValue.MINUS_ONE; } else { @@ -774,7 +775,7 @@ public TimeValue getRefreshInterval() { * Returns the transaction log retention size which controls how much of the translog is kept around to allow for ops based recoveries */ public ByteSizeValue getTranslogRetentionSize() { - assert softDeleteEnabled == false || translogRetentionSize.getBytes() == -1L : translogRetentionSize; + assert shouldDisableTranslogRetention(settings) == false || translogRetentionSize.getBytes() == -1L : translogRetentionSize; return translogRetentionSize; } @@ -783,7 +784,7 @@ public ByteSizeValue getTranslogRetentionSize() { * around */ public TimeValue getTranslogRetentionAge() { - assert softDeleteEnabled == false || translogRetentionAge.millis() == -1L : translogRetentionSize; + assert shouldDisableTranslogRetention(settings) == false || translogRetentionAge.millis() == -1L : translogRetentionSize; return translogRetentionAge; } @@ -795,6 +796,11 @@ public int getTranslogRetentionTotalFiles() { return INDEX_TRANSLOG_RETENTION_TOTAL_FILES_SETTING.get(getSettings()); } + private static boolean shouldDisableTranslogRetention(Settings settings) { + return INDEX_SOFT_DELETES_SETTING.get(settings) + && IndexMetaData.SETTING_INDEX_VERSION_CREATED.get(settings).onOrAfter(Version.V_7_4_0); + } + /** * Returns the generation threshold size. As sequence numbers can cause multiple generations to * be preserved for rollback purposes, we want to keep the size of individual generations from diff --git a/server/src/main/java/org/elasticsearch/index/engine/Engine.java b/server/src/main/java/org/elasticsearch/index/engine/Engine.java index 75ef4e0d12d77..26ecd4b29f39d 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -66,6 +66,7 @@ import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndVersion; import org.elasticsearch.common.metrics.CounterMetric; import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ReleasableLock; import org.elasticsearch.index.VersionType; @@ -729,7 +730,7 @@ public enum SearcherScope { /** * Acquires a lock on the translog files and Lucene soft-deleted documents to prevent them from being trimmed */ - public abstract Closeable acquireRetentionLock(); + public abstract Closeable acquireHistoryRetentionLock(HistorySource historySource); /** * Creates a new history snapshot from Lucene for reading operations whose seqno in the requesting seqno range (both inclusive). @@ -742,19 +743,20 @@ public abstract Translog.Snapshot newChangesSnapshot(String source, MapperServic * Creates a new history snapshot for reading operations since {@code startingSeqNo} (inclusive). * The returned snapshot can be retrieved from either Lucene index or translog files. */ - public abstract Translog.Snapshot readHistoryOperations(String source, - MapperService mapperService, long startingSeqNo) throws IOException; + public abstract Translog.Snapshot readHistoryOperations(String reason, HistorySource historySource, + MapperService mapperService, long startingSeqNo) throws IOException; /** * Returns the estimated number of history operations whose seq# at least {@code startingSeqNo}(inclusive) in this engine. */ - public abstract int estimateNumberOfHistoryOperations(String source, - MapperService mapperService, long startingSeqNo) throws IOException; + public abstract int estimateNumberOfHistoryOperations(String reason, HistorySource historySource, + MapperService mapperService, long startingSeqNo) throws IOException; /** * Checks if this engine has every operations since {@code startingSeqNo}(inclusive) in its history (either Lucene or translog) */ - public abstract boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException; + public abstract boolean hasCompleteOperationHistory(String reason, HistorySource historySource, + MapperService mapperService, long startingSeqNo) throws IOException; /** * Gets the minimum retained sequence number for this engine. @@ -1795,7 +1797,8 @@ public IndexCommit getIndexCommit() { } } - public void onSettingsChanged() { + public void onSettingsChanged(TimeValue translogRetentionAge, ByteSizeValue translogRetentionSize, long softDeletesRetentionOps) { + } /** @@ -1929,4 +1932,11 @@ public interface TranslogRecoveryRunner { * to advance this marker to at least the given sequence number. */ public abstract void advanceMaxSeqNoOfUpdatesOrDeletes(long maxSeqNoOfUpdatesOnPrimary); + + /** + * Whether we should read history operations from translog or Lucene index + */ + public enum HistorySource { + TRANSLOG, INDEX + } } diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index b60c60b89b119..1e445c0b0ac85 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -68,6 +68,8 @@ import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndSeqNo; import org.elasticsearch.common.metrics.CounterMetric; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.util.concurrent.KeyedLock; import org.elasticsearch.common.util.concurrent.ReleasableLock; @@ -532,27 +534,31 @@ public void syncTranslog() throws IOException { * The returned snapshot can be retrieved from either Lucene index or translog files. */ @Override - public Translog.Snapshot readHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { - if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { - return newChangesSnapshot(source, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false); + public Translog.Snapshot readHistoryOperations(String reason, HistorySource historySource, + MapperService mapperService, long startingSeqNo) throws IOException { + if (historySource == HistorySource.INDEX) { + ensureSoftDeletesEnabled(); + return newChangesSnapshot(reason, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false); + } else { + return getTranslog().newSnapshotFromMinSeqNo(startingSeqNo); } - - return getTranslog().newSnapshotFromMinSeqNo(startingSeqNo); } /** * Returns the estimated number of history operations whose seq# at least the provided seq# in this engine. */ @Override - public int estimateNumberOfHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { - if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { - try (Translog.Snapshot snapshot = newChangesSnapshot(source, mapperService, Math.max(0, startingSeqNo), + public int estimateNumberOfHistoryOperations(String reason, HistorySource historySource, + MapperService mapperService, long startingSeqNo) throws IOException { + if (historySource == HistorySource.INDEX) { + ensureSoftDeletesEnabled(); + try (Translog.Snapshot snapshot = newChangesSnapshot(reason, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false)) { return snapshot.totalOperations(); } + } else { + return getTranslog().estimateTotalOperationsFromMinSeq(startingSeqNo); } - - return getTranslog().estimateTotalOperationsFromMinSeq(startingSeqNo); } @Override @@ -2479,15 +2485,15 @@ final void ensureCanFlush() { } } - public void onSettingsChanged() { + @Override + public void onSettingsChanged(TimeValue translogRetentionAge, ByteSizeValue translogRetentionSize, long softDeletesRetentionOps) { mergeScheduler.refreshConfig(); // config().isEnableGcDeletes() or config.getGcDeletesInMillis() may have changed: maybePruneDeletes(); final TranslogDeletionPolicy translogDeletionPolicy = translog.getDeletionPolicy(); - final IndexSettings indexSettings = engineConfig.getIndexSettings(); - translogDeletionPolicy.setRetentionAgeInMillis(indexSettings.getTranslogRetentionAge().getMillis()); - translogDeletionPolicy.setRetentionSizeInBytes(indexSettings.getTranslogRetentionSize().getBytes()); - softDeletesPolicy.setRetentionOperations(indexSettings.getSoftDeleteRetentionOperations()); + translogDeletionPolicy.setRetentionAgeInMillis(translogRetentionAge.millis()); + translogDeletionPolicy.setRetentionSizeInBytes(translogRetentionSize.getBytes()); + softDeletesPolicy.setRetentionOperations(softDeletesRetentionOps); } public MergeStats getMergeStats() { @@ -2594,12 +2600,17 @@ long getNumDocUpdates() { return numDocUpdates.count(); } + private void ensureSoftDeletesEnabled() { + if (softDeleteEnabled == false) { + assert false : "index " + shardId.getIndex() + " does not have soft-deletes enabled"; + throw new IllegalStateException("index " + shardId.getIndex() + " does not have soft-deletes enabled"); + } + } + @Override public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperService, long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException { - if (softDeleteEnabled == false) { - throw new IllegalStateException("accessing changes snapshot requires soft-deletes enabled"); - } + ensureSoftDeletesEnabled(); ensureOpen(); refreshIfNeeded(source, toSeqNo); Searcher searcher = acquireSearcher(source, SearcherScope.INTERNAL); @@ -2621,26 +2632,28 @@ public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperS } @Override - public boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException { - if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + public boolean hasCompleteOperationHistory(String reason, HistorySource historySource, + MapperService mapperService, long startingSeqNo) throws IOException { + if (historySource == HistorySource.INDEX) { + ensureSoftDeletesEnabled(); return getMinRetainedSeqNo() <= startingSeqNo; - } - - final long currentLocalCheckpoint = localCheckpointTracker.getProcessedCheckpoint(); - // avoid scanning translog if not necessary - if (startingSeqNo > currentLocalCheckpoint) { - return true; - } - final LocalCheckpointTracker tracker = new LocalCheckpointTracker(startingSeqNo, startingSeqNo - 1); - try (Translog.Snapshot snapshot = getTranslog().newSnapshotFromMinSeqNo(startingSeqNo)) { - Translog.Operation operation; - while ((operation = snapshot.next()) != null) { - if (operation.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - tracker.markSeqNoAsProcessed(operation.seqNo()); + } else { + final long currentLocalCheckpoint = localCheckpointTracker.getProcessedCheckpoint(); + // avoid scanning translog if not necessary + if (startingSeqNo > currentLocalCheckpoint) { + return true; + } + final LocalCheckpointTracker tracker = new LocalCheckpointTracker(startingSeqNo, startingSeqNo - 1); + try (Translog.Snapshot snapshot = getTranslog().newSnapshotFromMinSeqNo(startingSeqNo)) { + Translog.Operation operation; + while ((operation = snapshot.next()) != null) { + if (operation.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { + tracker.markSeqNoAsProcessed(operation.seqNo()); + } } } + return tracker.getProcessedCheckpoint() >= currentLocalCheckpoint; } - return tracker.getProcessedCheckpoint() >= currentLocalCheckpoint; } /** @@ -2648,13 +2661,14 @@ public boolean hasCompleteOperationHistory(String source, MapperService mapperSe * Operations whose seq# are at least this value should exist in the Lucene index. */ public final long getMinRetainedSeqNo() { - assert softDeleteEnabled : Thread.currentThread().getName(); + ensureSoftDeletesEnabled(); return softDeletesPolicy.getMinRetainedSeqNo(); } @Override - public Closeable acquireRetentionLock() { - if (softDeleteEnabled) { + public Closeable acquireHistoryRetentionLock(HistorySource historySource) { + if (historySource == HistorySource.INDEX) { + ensureSoftDeletesEnabled(); return softDeletesPolicy.acquireRetentionLock(); } else { return translog.acquireRetentionLock(); diff --git a/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java b/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java index df3c8e275f3c7..768655cf1c852 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java @@ -307,7 +307,7 @@ public void syncTranslog() { } @Override - public Closeable acquireRetentionLock() { + public Closeable acquireHistoryRetentionLock(HistorySource historySource) { return () -> {}; } @@ -317,21 +317,24 @@ public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperS if (engineConfig.getIndexSettings().isSoftDeleteEnabled() == false) { throw new IllegalStateException("accessing changes snapshot requires soft-deletes enabled"); } - return readHistoryOperations(source, mapperService, fromSeqNo); + return newEmptySnapshot(); } @Override - public Translog.Snapshot readHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { + public Translog.Snapshot readHistoryOperations(String reason, HistorySource historySource, + MapperService mapperService, long startingSeqNo) { return newEmptySnapshot(); } @Override - public int estimateNumberOfHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { + public int estimateNumberOfHistoryOperations(String reason, HistorySource historySource, + MapperService mapperService, long startingSeqNo) { return 0; } @Override - public boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException { + public boolean hasCompleteOperationHistory(String reason, HistorySource historySource, + MapperService mapperService, long startingSeqNo) { // we can do operation-based recovery if we don't have to replay any operation. return startingSeqNo > seqNoStats.getMaxSeqNo(); } diff --git a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java index 1c8599f66cf31..14e78973504bc 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java @@ -895,9 +895,10 @@ public ReplicationTracker( this.pendingInSync = new HashSet<>(); this.routingTable = null; this.replicationGroup = null; - this.hasAllPeerRecoveryRetentionLeases = indexSettings.getIndexVersionCreated().onOrAfter(Version.V_8_0_0) || + this.hasAllPeerRecoveryRetentionLeases = indexSettings.isSoftDeleteEnabled() && + (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_8_0_0) || (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_4_0) && - indexSettings.getIndexMetaData().getState() == IndexMetaData.State.OPEN); + indexSettings.getIndexMetaData().getState() == IndexMetaData.State.OPEN)); this.fileBasedRecoveryThreshold = IndexSettings.FILE_BASED_RECOVERY_THRESHOLD_SETTING.get(indexSettings.getSettings()); this.safeCommitInfoSupplier = safeCommitInfoSupplier; assert Version.V_EMPTY.equals(indexSettings.getIndexVersionCreated()) == false; @@ -1348,6 +1349,10 @@ private synchronized void setHasAllPeerRecoveryRetentionLeases() { assert invariant(); } + public synchronized boolean hasAllPeerRecoveryRetentionLeases() { + return hasAllPeerRecoveryRetentionLeases; + } + /** * Create any required peer-recovery retention leases that do not currently exist because we just did a rolling upgrade from a version * prior to {@link Version#V_7_4_0} that does not create peer-recovery retention leases. diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index 8185d8fad5f16..059961e1e5897 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -67,6 +67,7 @@ import org.elasticsearch.common.metrics.CounterMetric; import org.elasticsearch.common.metrics.MeanMetric; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.concurrent.AbstractRunnable; @@ -157,6 +158,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -268,6 +270,7 @@ Runnable getGlobalCheckpointSyncer() { private final AtomicLong lastSearcherAccess = new AtomicLong(); private final AtomicReference pendingRefreshLocation = new AtomicReference<>(); + private volatile boolean useRetentionLeasesInPeerRecovery; public IndexShard( final ShardRouting shardRouting, @@ -364,6 +367,7 @@ public boolean shouldCache(Query query) { refreshListeners = buildRefreshListeners(); lastSearcherAccess.set(threadPool.relativeTimeInMillis()); persistMetadata(path, indexSettings, shardRouting, null, logger); + this.useRetentionLeasesInPeerRecovery = replicationTracker.hasAllPeerRecoveryRetentionLeases(); } public ThreadPool getThreadPool() { @@ -600,6 +604,17 @@ public void onFailure(Exception e) { if (newRouting.equals(currentRouting) == false) { indexEventListener.shardRoutingChanged(this, currentRouting, newRouting); } + + if (indexSettings.isSoftDeleteEnabled() && useRetentionLeasesInPeerRecovery == false) { + final RetentionLeases retentionLeases = replicationTracker.getRetentionLeases(); + final Set shardRoutings = new HashSet<>(routingTable.getShards()); + shardRoutings.addAll(routingTable.assignedShards()); // include relocation targets + if (shardRoutings.stream().allMatch( + shr -> shr.assignedToNode() && retentionLeases.contains(ReplicationTracker.getPeerRecoveryRetentionLeaseId(shr)))) { + useRetentionLeasesInPeerRecovery = true; + turnOffTranslogRetention(); + } + } } /** @@ -1877,38 +1892,63 @@ boolean shouldRollTranslogGeneration() { public void onSettingsChanged() { Engine engineOrNull = getEngineOrNull(); if (engineOrNull != null) { - engineOrNull.onSettingsChanged(); + final boolean useRetentionLeasesInPeerRecovery = this.useRetentionLeasesInPeerRecovery; + engineOrNull.onSettingsChanged( + useRetentionLeasesInPeerRecovery ? TimeValue.MINUS_ONE : indexSettings.getTranslogRetentionAge(), + useRetentionLeasesInPeerRecovery ? new ByteSizeValue(-1) : indexSettings.getTranslogRetentionSize(), + indexSettings.getSoftDeleteRetentionOperations() + ); } } + private void turnOffTranslogRetention() { + logger.debug("turn off the translog retention for the replication group {} " + + "as it starts using retention leases exclusively in peer recoveries", shardId); + // Off to the generic threadPool as pruning the delete tombstones can be expensive. + threadPool.generic().execute(new AbstractRunnable() { + @Override + public void onFailure(Exception e) { + if (state != IndexShardState.CLOSED) { + logger.warn("failed to turn off translog retention", e); + } + } + + @Override + protected void doRun() { + onSettingsChanged(); + trimTranslog(); + } + }); + } + /** * Acquires a lock on the translog files and Lucene soft-deleted documents to prevent them from being trimmed */ - public Closeable acquireRetentionLock() { - return getEngine().acquireRetentionLock(); + public Closeable acquireHistoryRetentionLock(Engine.HistorySource source) { + return getEngine().acquireHistoryRetentionLock(source); } /** * Returns the estimated number of history operations whose seq# at least the provided seq# in this shard. */ - public int estimateNumberOfHistoryOperations(String source, long startingSeqNo) throws IOException { - return getEngine().estimateNumberOfHistoryOperations(source, mapperService, startingSeqNo); + public int estimateNumberOfHistoryOperations(String reason, Engine.HistorySource source, long startingSeqNo) throws IOException { + return getEngine().estimateNumberOfHistoryOperations(reason, source, mapperService, startingSeqNo); } /** * Creates a new history snapshot for reading operations since the provided starting seqno (inclusive). * The returned snapshot can be retrieved from either Lucene index or translog files. */ - public Translog.Snapshot getHistoryOperations(String source, long startingSeqNo) throws IOException { - return getEngine().readHistoryOperations(source, mapperService, startingSeqNo); + public Translog.Snapshot getHistoryOperations(String reason, Engine.HistorySource source, long startingSeqNo) throws IOException { + return getEngine().readHistoryOperations(reason, source, mapperService, startingSeqNo); } /** * Checks if we have a completed history of operations since the given starting seqno (inclusive). - * This method should be called after acquiring the retention lock; See {@link #acquireRetentionLock()} + * This method should be called after acquiring the retention lock; See {@link #acquireHistoryRetentionLock(Engine.HistorySource)} */ - public boolean hasCompleteHistoryOperations(String source, long startingSeqNo) throws IOException { - return getEngine().hasCompleteOperationHistory(source, mapperService, startingSeqNo); + public boolean hasCompleteHistoryOperations(String reason, Engine.HistorySource source, long startingSeqNo) throws IOException { + return getEngine().hasCompleteOperationHistory(reason, source, mapperService, startingSeqNo); } /** @@ -2097,9 +2137,9 @@ public RetentionLease addRetentionLease( assert assertPrimaryMode(); verifyNotClosed(); ensureSoftDeletesEnabled("retention leases"); - try (Closeable ignore = acquireRetentionLock()) { + try (Closeable ignore = acquireHistoryRetentionLock(Engine.HistorySource.INDEX)) { final long actualRetainingSequenceNumber = - retainingSequenceNumber == RETAIN_ALL ? getMinRetainedSeqNo() : retainingSequenceNumber; + retainingSequenceNumber == RETAIN_ALL ? getMinRetainedSeqNo() : retainingSequenceNumber; return replicationTracker.addRetentionLease(id, actualRetainingSequenceNumber, source, listener); } catch (final IOException e) { throw new AssertionError(e); @@ -2119,7 +2159,7 @@ public RetentionLease renewRetentionLease(final String id, final long retainingS assert assertPrimaryMode(); verifyNotClosed(); ensureSoftDeletesEnabled("retention leases"); - try (Closeable ignore = acquireRetentionLock()) { + try (Closeable ignore = acquireHistoryRetentionLock(Engine.HistorySource.INDEX)) { final long actualRetainingSequenceNumber = retainingSequenceNumber == RETAIN_ALL ? getMinRetainedSeqNo() : retainingSequenceNumber; return replicationTracker.renewRetentionLease(id, actualRetainingSequenceNumber, source); @@ -2600,6 +2640,10 @@ public List getPeerRecoveryRetentionLeases() { return replicationTracker.getPeerRecoveryRetentionLeases(); } + public boolean useRetentionLeasesInPeerRecovery() { + return useRetentionLeasesInPeerRecovery; + } + private SafeCommitInfo getSafeCommitInfo() { final Engine engine = getEngineOrNull(); return engine == null ? SafeCommitInfo.EMPTY : engine.getSafeCommitInfo(); diff --git a/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java b/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java index f4cd1cdb8115e..710c78a2d294e 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java +++ b/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java @@ -36,6 +36,7 @@ import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.tasks.Task; @@ -90,7 +91,9 @@ public void resync(final IndexShard indexShard, final ActionListener // Wrap translog snapshot to make it synchronized as it is accessed by different threads through SnapshotSender. // Even though those calls are not concurrent, snapshot.next() uses non-synchronized state and is not multi-thread-compatible // Also fail the resync early if the shard is shutting down - snapshot = indexShard.getHistoryOperations("resync", startingSeqNo); + snapshot = indexShard.getHistoryOperations("resync", + indexShard.indexSettings.isSoftDeleteEnabled() ? Engine.HistorySource.INDEX : Engine.HistorySource.TRANSLOG, + startingSeqNo); final Translog.Snapshot originalSnapshot = snapshot; final Translog.Snapshot wrappedSnapshot = new Translog.Snapshot() { @Override diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java index c372cc4571a7c..285edc329be06 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java @@ -169,22 +169,28 @@ public void recoverToTarget(ActionListener listener) { ReplicationTracker.getPeerRecoveryRetentionLeaseId(targetShardRouting)) : null); }, shardId + " validating recovery target ["+ request.targetAllocationId() + "] registered ", shard, cancellableThreads, logger); - final Closeable retentionLock = shard.acquireRetentionLock(); + final Engine.HistorySource historySource; + if (shard.useRetentionLeasesInPeerRecovery() || retentionLeaseRef.get() != null) { + historySource = Engine.HistorySource.INDEX; + } else { + historySource = Engine.HistorySource.TRANSLOG; + } + final Closeable retentionLock = shard.acquireHistoryRetentionLock(historySource); resources.add(retentionLock); final long startingSeqNo; final boolean isSequenceNumberBasedRecovery = request.startingSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && isTargetSameHistory() - && shard.hasCompleteHistoryOperations("peer-recovery", request.startingSeqNo()) - && (softDeletesEnabled == false - || (retentionLeaseRef.get() != null && retentionLeaseRef.get().retainingSequenceNumber() <= request.startingSeqNo())); + && shard.hasCompleteHistoryOperations("peer-recovery", historySource, request.startingSeqNo()) + && (historySource == Engine.HistorySource.TRANSLOG || + (retentionLeaseRef.get() != null && retentionLeaseRef.get().retainingSequenceNumber() <= request.startingSeqNo())); // NB check hasCompleteHistoryOperations when computing isSequenceNumberBasedRecovery, even if there is a retention lease, // because when doing a rolling upgrade from earlier than 7.4 we may create some leases that are initially unsatisfied. It's // possible there are other cases where we cannot satisfy all leases, because that's not a property we currently expect to hold. // Also it's pretty cheap when soft deletes are enabled, and it'd be a disaster if we tried a sequence-number-based recovery // without having a complete history. - if (isSequenceNumberBasedRecovery && softDeletesEnabled) { + if (isSequenceNumberBasedRecovery && retentionLeaseRef.get() != null) { // all the history we need is retained by an existing retention lease, so we do not need a separate retention lock retentionLock.close(); logger.trace("history is retained by {}", retentionLeaseRef.get()); @@ -203,7 +209,11 @@ && isTargetSameHistory() if (isSequenceNumberBasedRecovery) { logger.trace("performing sequence numbers based recovery. starting at [{}]", request.startingSeqNo()); startingSeqNo = request.startingSeqNo(); - sendFileStep.onResponse(SendFileResult.EMPTY); + if (softDeletesEnabled && retentionLeaseRef.get() == null) { + createRetentionLease(startingSeqNo, ActionListener.map(sendFileStep, ignored -> SendFileResult.EMPTY)); + } else { + sendFileStep.onResponse(SendFileResult.EMPTY); + } } else { final Engine.IndexCommitRef safeCommitRef; try { @@ -229,7 +239,7 @@ && isTargetSameHistory() logger.trace("performing file-based recovery followed by history replay starting at [{}]", startingSeqNo); try { - final int estimateNumOps = shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo); + final int estimateNumOps = shard.estimateNumberOfHistoryOperations("peer-recovery", historySource, startingSeqNo); final Releasable releaseStore = acquireStore(shard.store()); resources.add(releaseStore); sendFileStep.whenComplete(r -> IOUtils.close(safeCommitRef, releaseStore), e -> { @@ -282,7 +292,8 @@ && isTargetSameHistory() sendFileStep.whenComplete(r -> { assert Transports.assertNotTransportThread(RecoverySourceHandler.this + "[prepareTargetForTranslog]"); // For a sequence based recovery, the target can keep its local translog - prepareTargetForTranslog(shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo), prepareEngineStep); + prepareTargetForTranslog( + shard.estimateNumberOfHistoryOperations("peer-recovery", historySource, startingSeqNo), prepareEngineStep); }, onFailure); prepareEngineStep.whenComplete(prepareEngineTime -> { @@ -298,14 +309,10 @@ && isTargetSameHistory() final long endingSeqNo = shard.seqNoStats().getMaxSeqNo(); logger.trace("snapshot translog for recovery; current size is [{}]", - shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo)); - final Translog.Snapshot phase2Snapshot = shard.getHistoryOperations("peer-recovery", startingSeqNo); + shard.estimateNumberOfHistoryOperations("peer-recovery", historySource, startingSeqNo)); + final Translog.Snapshot phase2Snapshot = shard.getHistoryOperations("peer-recovery", historySource, startingSeqNo); resources.add(phase2Snapshot); - - if (softDeletesEnabled == false || isSequenceNumberBasedRecovery == false) { - // we can release the retention lock here because the snapshot itself will retain the required operations. - retentionLock.close(); - } + retentionLock.close(); // we have to capture the max_seen_auto_id_timestamp and the max_seq_no_of_updates to make sure that these values // are at least as high as the corresponding values on the primary when any of these operations were executed on it. diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java index 7e0e5f7cf17c8..04d347d0006b1 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java @@ -316,7 +316,9 @@ public void finalizeRecovery(final long globalCheckpoint, final long trimAboveSe private boolean hasUncommittedOperations() throws IOException { long localCheckpointOfCommit = Long.parseLong(indexShard.commitStats().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); - return indexShard.estimateNumberOfHistoryOperations("peer-recovery", localCheckpointOfCommit + 1) > 0; + return indexShard.estimateNumberOfHistoryOperations("peer-recovery", + indexShard.indexSettings().isSoftDeleteEnabled() ? Engine.HistorySource.INDEX : Engine.HistorySource.TRANSLOG, + localCheckpointOfCommit + 1) > 0; } @Override diff --git a/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java b/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java index e64e7f42a9a2e..1a19647ea7f78 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java @@ -549,7 +549,7 @@ public void testSoftDeletesDefaultSetting() { public void testIgnoreTranslogRetentionSettingsIfSoftDeletesEnabled() { Settings.Builder settings = Settings.builder() - .put(IndexMetaData.SETTING_VERSION_CREATED, VersionUtils.randomIndexCompatibleVersion(random())); + .put(IndexMetaData.SETTING_VERSION_CREATED, VersionUtils.randomVersionBetween(random(), Version.V_7_4_0, Version.CURRENT)); if (randomBoolean()) { settings.put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), randomPositiveTimeValue()); } diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index dc37022c130fe..c38f426dbfb59 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -376,7 +376,8 @@ public void testSegmentsWithoutSoftDeletes() throws Exception { assertThat(segments.get(1).getDeletedDocs(), equalTo(0)); assertThat(segments.get(1).isCompound(), equalTo(true)); - engine.onSettingsChanged(); + engine.onSettingsChanged(indexSettings.getTranslogRetentionAge(), indexSettings.getTranslogRetentionSize(), + indexSettings.getSoftDeleteRetentionOperations()); ParsedDocument doc4 = testParsedDocument("4", null, testDocumentWithTextField(), B_3, null); engine.index(indexForDoc(doc4)); engine.refresh("test"); @@ -1623,7 +1624,8 @@ public void testForceMergeWithSoftDeletesRetention() throws Exception { } settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0); indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); - engine.onSettingsChanged(); + engine.onSettingsChanged(indexSettings.getTranslogRetentionAge(), indexSettings.getTranslogRetentionSize(), + indexSettings.getSoftDeleteRetentionOperations()); globalCheckpoint.set(localCheckpoint); engine.syncTranslog(); @@ -1714,7 +1716,8 @@ public void testForceMergeWithSoftDeletesRetentionAndRecoverySource() throws Exc } settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0); indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); - engine.onSettingsChanged(); + engine.onSettingsChanged(indexSettings.getTranslogRetentionAge(), indexSettings.getTranslogRetentionSize(), + indexSettings.getSoftDeleteRetentionOperations()); // If we already merged down to 1 segment, then the next force-merge will be a noop. We need to add an extra segment to make // merges happen so we can verify that _recovery_source are pruned. See: https://github.com/elastic/elasticsearch/issues/41628. final int numSegments; @@ -5040,7 +5043,8 @@ public void testShouldPeriodicallyFlush() throws Exception { .settings(Settings.builder().put(indexSettings.getSettings()) .put(IndexSettings.INDEX_TRANSLOG_FLUSH_THRESHOLD_SIZE_SETTING.getKey(), flushThreshold + "b")).build(); indexSettings.updateIndexMetaData(indexMetaData); - engine.onSettingsChanged(); + engine.onSettingsChanged(indexSettings.getTranslogRetentionAge(), indexSettings.getTranslogRetentionSize(), + indexSettings.getSoftDeleteRetentionOperations()); assertThat(engine.getTranslog().stats().getUncommittedOperations(), equalTo(numDocs)); assertThat(engine.shouldPeriodicallyFlush(), equalTo(true)); engine.flush(); @@ -5088,7 +5092,8 @@ public void testShouldPeriodicallyFlushAfterMerge() throws Exception { .settings(Settings.builder().put(indexSettings.getSettings()) .put(IndexSettings.INDEX_FLUSH_AFTER_MERGE_THRESHOLD_SIZE_SETTING.getKey(), "0b")).build(); indexSettings.updateIndexMetaData(indexMetaData); - engine.onSettingsChanged(); + engine.onSettingsChanged(indexSettings.getTranslogRetentionAge(), indexSettings.getTranslogRetentionSize(), + indexSettings.getSoftDeleteRetentionOperations()); assertThat(engine.getTranslog().stats().getUncommittedOperations(), equalTo(1)); assertThat(engine.shouldPeriodicallyFlush(), equalTo(false)); doc = testParsedDocument(Integer.toString(1), null, testDocumentWithTextField(), SOURCE, null); @@ -5113,7 +5118,8 @@ public void testStressShouldPeriodicallyFlush() throws Exception { .put(IndexSettings.INDEX_TRANSLOG_GENERATION_THRESHOLD_SIZE_SETTING.getKey(), generationThreshold + "b") .put(IndexSettings.INDEX_TRANSLOG_FLUSH_THRESHOLD_SIZE_SETTING.getKey(), flushThreshold + "b")).build(); indexSettings.updateIndexMetaData(indexMetaData); - engine.onSettingsChanged(); + engine.onSettingsChanged(indexSettings.getTranslogRetentionAge(), indexSettings.getTranslogRetentionSize(), + indexSettings.getSoftDeleteRetentionOperations()); final int numOps = scaledRandomIntBetween(100, 10_000); for (int i = 0; i < numOps; i++) { final long localCheckPoint = engine.getProcessedLocalCheckpoint(); @@ -5141,7 +5147,8 @@ public void testStressUpdateSameDocWhileGettingIt() throws IOException, Interrup .settings(Settings.builder().put(indexSettings.getSettings()) .put(IndexSettings.INDEX_GC_DELETES_SETTING.getKey(), TimeValue.timeValueMillis(1))).build(); engine.engineConfig.getIndexSettings().updateIndexMetaData(indexMetaData); - engine.onSettingsChanged(); + engine.onSettingsChanged(indexSettings.getTranslogRetentionAge(), indexSettings.getTranslogRetentionSize(), + indexSettings.getSoftDeleteRetentionOperations()); ParsedDocument document = testParsedDocument(Integer.toString(0), null, testDocumentWithTextField(), SOURCE, null); final Engine.Index doc = new Engine.Index(newUid(document), document, UNASSIGNED_SEQ_NO, 0, Versions.MATCH_ANY, VersionType.INTERNAL, Engine.Operation.Origin.PRIMARY, System.nanoTime(), @@ -5411,7 +5418,8 @@ public void testKeepMinRetainedSeqNoByMergePolicy() throws IOException { if (rarely()) { settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); - engine.onSettingsChanged(); + engine.onSettingsChanged(indexSettings.getTranslogRetentionAge(), indexSettings.getTranslogRetentionSize(), + indexSettings.getSoftDeleteRetentionOperations()); } if (rarely()) { engine.refresh("test"); @@ -5424,7 +5432,7 @@ public void testKeepMinRetainedSeqNoByMergePolicy() throws IOException { if (rarely()) { engine.forceMerge(randomBoolean()); } - try (Closeable ignored = engine.acquireRetentionLock()) { + try (Closeable ignored = engine.acquireHistoryRetentionLock(Engine.HistorySource.INDEX)) { long minRetainSeqNos = engine.getMinRetainedSeqNo(); assertThat(minRetainSeqNos, lessThanOrEqualTo(globalCheckpoint.get() + 1)); Long[] expectedOps = existingSeqNos.stream().filter(seqno -> seqno >= minRetainSeqNos).toArray(Long[]::new); @@ -5705,9 +5713,9 @@ public void testRequireSoftDeletesWhenAccessingChangesSnapshot() throws Exceptio IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(Settings.builder(). put(defaultSettings.getSettings()).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false)).build()); try (InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null))) { - IllegalStateException error = expectThrows(IllegalStateException.class, + AssertionError error = expectThrows(AssertionError.class, () -> engine.newChangesSnapshot("test", createMapperService(), 0, randomNonNegativeLong(), randomBoolean())); - assertThat(error.getMessage(), equalTo("accessing changes snapshot requires soft-deletes enabled")); + assertThat(error.getMessage(), containsString("does not have soft-deletes enabled")); } } } diff --git a/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java b/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java index 4882b9262d98c..f565459b26eba 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java @@ -470,7 +470,8 @@ public long addDocument(Iterable doc) throws IOExcepti assertThat(snapshot.totalOperations(), equalTo(0)); } } - try (Translog.Snapshot snapshot = shard.getHistoryOperations("test", 0)) { + try (Translog.Snapshot snapshot = shard.getHistoryOperations( + "test", shard.indexSettings().isSoftDeleteEnabled() ? Engine.HistorySource.INDEX : Engine.HistorySource.TRANSLOG, 0)) { assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); } } @@ -488,7 +489,8 @@ public long addDocument(Iterable doc) throws IOExcepti assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(Collections.singletonList(noop2))); } } - try (Translog.Snapshot snapshot = shard.getHistoryOperations("test", 0)) { + try (Translog.Snapshot snapshot = shard.getHistoryOperations( + "test", shard.indexSettings().isSoftDeleteEnabled() ? Engine.HistorySource.INDEX : Engine.HistorySource.TRANSLOG, 0)) { assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); } } @@ -585,7 +587,8 @@ public void testSeqNoCollision() throws Exception { shards.promoteReplicaToPrimary(replica2).get(); logger.info("--> Recover replica3 from replica2"); recoverReplica(replica3, replica2, true); - try (Translog.Snapshot snapshot = replica3.getHistoryOperations("test", 0)) { + try (Translog.Snapshot snapshot = replica3.getHistoryOperations( + "test", replica3.indexSettings().isSoftDeleteEnabled() ? Engine.HistorySource.INDEX : Engine.HistorySource.TRANSLOG, 0)) { assertThat(snapshot.totalOperations(), equalTo(initDocs + 1)); final List expectedOps = new ArrayList<>(initOperations); expectedOps.add(op2); diff --git a/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java b/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java index 75de0bb677296..7ca6c1ef1bf7f 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java @@ -32,6 +32,7 @@ import org.elasticsearch.index.seqno.RetentionLeases; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.test.VersionUtils; import java.util.ArrayList; import java.util.List; @@ -147,6 +148,27 @@ protected void syncRetentionLeases(ShardId shardId, RetentionLeases leases, Acti } } + public void testTurnOffTranslogRetentionAfterAllShardStarted() throws Exception { + final Settings.Builder settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true); + if (randomBoolean()) { + settings.put(IndexMetaData.SETTING_VERSION_CREATED, VersionUtils.randomIndexCompatibleVersion(random())); + } + try (ReplicationGroup group = createGroup(between(1, 2), settings.build())) { + group.startAll(); + group.indexDocs(randomIntBetween(1, 10)); + for (IndexShard shard : group) { + shard.updateShardState(shard.routingEntry(), shard.getOperationPrimaryTerm(), null, 1L, + group.getPrimary().getReplicationGroup().getInSyncAllocationIds(), + group.getPrimary().getReplicationGroup().getRoutingTable()); + } + group.syncGlobalCheckpoint(); + group.flush(); + for (IndexShard shard : group) { + assertThat(shard.translogStats().estimatedNumberOfOperations(), equalTo(0)); + } + } + } + static final class SyncRetentionLeasesResponse extends ReplicationResponse { final RetentionLeaseSyncAction.Request syncRequest; SyncRetentionLeasesResponse(RetentionLeaseSyncAction.Request syncRequest) { diff --git a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseIT.java b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseIT.java index a52f816ff6df2..831422f8dad86 100644 --- a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseIT.java +++ b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseIT.java @@ -30,6 +30,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.indices.IndicesService; @@ -110,7 +111,7 @@ public void testRetentionLeasesSyncedOnAdd() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ActionListener listener = countDownLatchListener(latch); // simulate a peer recovery which locks the soft deletes policy on the primary - final Closeable retentionLock = randomBoolean() ? primary.acquireRetentionLock() : () -> {}; + final Closeable retentionLock = randomBoolean() ? primary.acquireHistoryRetentionLock(Engine.HistorySource.INDEX) : () -> {}; currentRetentionLeases.put(id, primary.addRetentionLease(id, retainingSequenceNumber, source, listener)); latch.await(); retentionLock.close(); @@ -160,7 +161,7 @@ public void testRetentionLeaseSyncedOnRemove() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ActionListener listener = countDownLatchListener(latch); // simulate a peer recovery which locks the soft deletes policy on the primary - final Closeable retentionLock = randomBoolean() ? primary.acquireRetentionLock() : () -> {}; + final Closeable retentionLock = randomBoolean() ? primary.acquireHistoryRetentionLock(Engine.HistorySource.INDEX) : () -> {}; currentRetentionLeases.put(id, primary.addRetentionLease(id, retainingSequenceNumber, source, listener)); latch.await(); retentionLock.close(); @@ -171,7 +172,7 @@ public void testRetentionLeaseSyncedOnRemove() throws Exception { final CountDownLatch latch = new CountDownLatch(1); primary.removeRetentionLease(id, countDownLatchListener(latch)); // simulate a peer recovery which locks the soft deletes policy on the primary - final Closeable retentionLock = randomBoolean() ? primary.acquireRetentionLock() : () -> {}; + final Closeable retentionLock = randomBoolean() ? primary.acquireHistoryRetentionLock(Engine.HistorySource.INDEX) : () -> {}; currentRetentionLeases.remove(id); latch.await(); retentionLock.close(); diff --git a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java index d6cd94cd07aaa..3099795b138d9 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java @@ -40,6 +40,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.VersionType; +import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.mapper.SourceToParse; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.translog.TestTranslog; @@ -63,6 +64,7 @@ import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -210,7 +212,9 @@ public void testDoNotSendOperationsWithoutSequenceNumber() throws Exception { operations.add(new Translog.Index( Integer.toString(i), randomBoolean() ? SequenceNumbers.UNASSIGNED_SEQ_NO : i, primaryTerm, new byte[]{1})); } - doReturn(TestTranslog.newSnapshotFromOperations(operations)).when(shard).getHistoryOperations(anyString(), anyLong()); + Engine.HistorySource source = + shard.indexSettings.isSoftDeleteEnabled() ? Engine.HistorySource.INDEX : Engine.HistorySource.TRANSLOG; + doReturn(TestTranslog.newSnapshotFromOperations(operations)).when(shard).getHistoryOperations(anyString(), eq(source), anyLong()); TaskManager taskManager = new TaskManager(Settings.EMPTY, threadPool, Collections.emptySet()); List sentOperations = new ArrayList<>(); PrimaryReplicaSyncer.SyncAction syncAction = (request, parentTask, allocationId, primaryTerm, listener) -> { diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java index 35f431e84eb9b..05aebcc459a6e 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java @@ -245,7 +245,7 @@ public void testRecoveryWithOutOfOrderDeleteWithSoftDeletes() throws Exception { IndexShard newReplica = shards.addReplicaWithExistingPath(orgPrimary.shardPath(), orgPrimary.routingEntry().currentNodeId()); shards.recoverReplica(newReplica); shards.assertAllEqual(3); - try (Translog.Snapshot snapshot = newReplica.getHistoryOperations("test", 0)) { + try (Translog.Snapshot snapshot = newReplica.getHistoryOperations("test", Engine.HistorySource.INDEX, 0)) { assertThat(snapshot, SnapshotMatchers.size(6)); } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index 965e4ae37fa97..1eb52ac6de769 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -55,6 +55,7 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.snapshots.SnapshotState; import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -1062,4 +1063,78 @@ private static boolean isXPackTemplate(String name) { return false; } } + + public void flush(String index, boolean force) throws IOException { + logger.info("flushing index {} force={}", index, force); + final Request flushRequest = new Request("POST", "/" + index + "/_flush"); + flushRequest.addParameter("force", Boolean.toString(force)); + flushRequest.addParameter("wait_if_ongoing", "true"); + assertOK(client().performRequest(flushRequest)); + } + + /** + * Asserts that replicas on nodes satisfying the {@code targetNode} should have perform operation-based recoveries. + */ + public void assertNoFileBasedRecovery(String indexName, Predicate targetNode) throws IOException { + Map recoveries = entityAsMap(client().performRequest(new Request("GET", indexName + "/_recovery?detailed=true"))); + @SuppressWarnings("unchecked") + List> shards = (List>) XContentMapValues.extractValue(indexName + ".shards", recoveries); + assertNotNull(shards); + boolean foundReplica = false; + logger.info("index {} recovery stats {}", indexName, shards); + for (Map shard : shards) { + if (shard.get("primary") == Boolean.FALSE && targetNode.test((String) XContentMapValues.extractValue("target.name", shard))) { + List details = (List) XContentMapValues.extractValue("index.files.details", shard); + // once detailed recoveries works, remove this if. + if (details == null) { + long totalFiles = ((Number) XContentMapValues.extractValue("index.files.total", shard)).longValue(); + long reusedFiles = ((Number) XContentMapValues.extractValue("index.files.reused", shard)).longValue(); + logger.info("total [{}] reused [{}]", totalFiles, reusedFiles); + assertThat("must reuse all files, recoveries [" + recoveries + "]", totalFiles, equalTo(reusedFiles)); + } else { + assertNotNull(details); + assertThat(details, Matchers.empty()); + } + foundReplica = true; + } + } + assertTrue("must find replica", foundReplica); + } + + /** + * Asserts that we do not retain any extra translog for the given index (i.e., turn off the translog retention) + */ + public void assertEmptyTranslog(String index) throws Exception { + Map stats = entityAsMap(client().performRequest(new Request("GET", index + "/_stats?level=shards"))); + assertThat(XContentMapValues.extractValue("indices." + index + ".total.translog.uncommitted_operations", stats), equalTo(0)); + assertThat(XContentMapValues.extractValue("indices." + index + ".total.translog.operations", stats), equalTo(0)); + } + + /** + * Peer recovery retention leases are renewed and synced to replicas periodically (every 30 seconds). This ensures + * that we have renewed every PRRL to the global checkpoint of the corresponding copy and properly synced to all copies. + */ + public void ensurePeerRecoveryRetentionLeasesRenewedAndSynced(String index) throws Exception { + assertBusy(() -> { + Map stats = entityAsMap(client().performRequest(new Request("GET", index + "/_stats?level=shards"))); + @SuppressWarnings("unchecked") Map>> shards = + (Map>>) XContentMapValues.extractValue("indices." + index + ".shards", stats); + for (List> shard : shards.values()) { + for (Map copy : shard) { + Integer globalCheckpoint = (Integer) XContentMapValues.extractValue("seq_no.global_checkpoint", copy); + assertNotNull(globalCheckpoint); + @SuppressWarnings("unchecked") List> retentionLeases = + (List>) XContentMapValues.extractValue("retention_leases.leases", copy); + if (retentionLeases == null) { + continue; + } + for (Map retentionLease : retentionLeases) { + if (((String) retentionLease.get("id")).startsWith("peer_recovery/")) { + assertThat(retentionLease.get("retaining_seq_no"), equalTo(globalCheckpoint + 1)); + } + } + } + } + }, 60, TimeUnit.SECONDS); + } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java index b14403406dd66..2510a177056d3 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.Randomness; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShardTestCase; @@ -75,7 +76,7 @@ public void testPrimaryTermFromFollower() throws IOException { operations, numOps - 1, followerPrimary, logger); - try (Translog.Snapshot snapshot = followerPrimary.getHistoryOperations("test", 0)) { + try (Translog.Snapshot snapshot = followerPrimary.getHistoryOperations("test", Engine.HistorySource.INDEX, 0)) { assertThat(snapshot.totalOperations(), equalTo(operations.size())); Translog.Operation operation; while ((operation = snapshot.next()) != null) { From 059cf8c561d713af9ca03b0fa4cf511a53c293c8 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 13 Dec 2019 17:05:34 -0500 Subject: [PATCH 205/686] Fix testTurnOffTranslogRetentionAfterAllShardStarted We turn off the translog retention policy asynchronously using the generic threadpool; hence, we need to assert busily here Relates #49448 --- .../replication/RetentionLeasesReplicationTests.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java b/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java index 7ca6c1ef1bf7f..d6c51c686cca7 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java @@ -163,9 +163,12 @@ public void testTurnOffTranslogRetentionAfterAllShardStarted() throws Exception } group.syncGlobalCheckpoint(); group.flush(); - for (IndexShard shard : group) { - assertThat(shard.translogStats().estimatedNumberOfOperations(), equalTo(0)); - } + assertBusy(() -> { + // we turn off the translog retention policy using the generic threadPool + for (IndexShard shard : group) { + assertThat(shard.translogStats().estimatedNumberOfOperations(), equalTo(0)); + } + }); } } From 31906b3f1a9cbaf4c488a85682c367226308b24a Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Sat, 14 Dec 2019 21:14:41 -0500 Subject: [PATCH 206/686] Adjust bwc for #48430 Relates #48430 --- .../java/org/elasticsearch/index/seqno/ReplicationTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java index 14e78973504bc..8c42784b88f82 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java @@ -896,7 +896,7 @@ public ReplicationTracker( this.routingTable = null; this.replicationGroup = null; this.hasAllPeerRecoveryRetentionLeases = indexSettings.isSoftDeleteEnabled() && - (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_8_0_0) || + (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_6_0) || (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_4_0) && indexSettings.getIndexMetaData().getState() == IndexMetaData.State.OPEN)); this.fileBasedRecoveryThreshold = IndexSettings.FILE_BASED_RECOVERY_THRESHOLD_SETTING.get(indexSettings.getSettings()); From 8b33f1509e19e203da40e4c5fbc845cb87be0760 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Sun, 15 Dec 2019 21:42:20 +0000 Subject: [PATCH 207/686] Change process kill order for testclusters shutdown (#50175) The testclusters shutdown code was killing child processes of the ES JVM before the ES JVM. This causes any running ML jobs to be recorded as failed, as the ES JVM notices that they have disconnected from it without being told to stop, as they would if they crashed. In many test suites this doesn't matter because the test cluster will never be restarted, but in the case of upgrade tests it makes it impossible to test what happens when an ML job is running at the time of the upgrade. This change reverses the order of killing the ES process tree such that the parent processes are killed before their children. A list of children is stored before killing the parent so that they can subsequently be killed (if they don't exit by themselves as a side effect of the parent dying). Fixes #46262 --- .../testclusters/ElasticsearchNode.java | 51 ++++++++++--------- .../upgrades/MlMappingsUpgradeIT.java | 1 - 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java index 2f258733e7575..f43f28190da1a 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java @@ -807,6 +807,7 @@ public synchronized void stop(boolean tailLogs) { requireNonNull(esProcess, "Can't stop `" + this + "` as it was not started or already stopped."); // Test clusters are not reused, don't spend time on a graceful shutdown stopHandle(esProcess.toHandle(), true); + reaper.unregister(toString()); if (tailLogs) { logFileContents("Standard output of node", esStdoutFile); logFileContents("Standard error of node", esStderrFile); @@ -831,39 +832,43 @@ public void setNameCustomization(Function nameCustomizer) { } private void stopHandle(ProcessHandle processHandle, boolean forcibly) { - // Stop all children first, ES could actually be a child when there's some wrapper process like on Windows. + // No-op if the process has already exited by itself. if (processHandle.isAlive() == false) { LOGGER.info("Process was not running when we tried to terminate it."); return; } - // Stop all children first, ES could actually be a child when there's some wrapper process like on Windows. - processHandle.children().forEach(each -> stopHandle(each, forcibly)); + // Stop all children last - if the ML processes are killed before the ES JVM then + // they'll be recorded as having failed and won't restart when the cluster restarts. + // ES could actually be a child when there's some wrapper process like on Windows, + // and in that case the ML processes will be grandchildren of the wrapper. + List children = processHandle.children().collect(Collectors.toList()); + try { + logProcessInfo( + "Terminating elasticsearch process" + (forcibly ? " forcibly " : "gracefully") + ":", + processHandle.info() + ); - logProcessInfo( - "Terminating elasticsearch process" + (forcibly ? " forcibly " : "gracefully") + ":", - processHandle.info() - ); + if (forcibly) { + processHandle.destroyForcibly(); + } else { + processHandle.destroy(); + waitForProcessToExit(processHandle); + if (processHandle.isAlive() == false) { + return; + } + LOGGER.info("process did not terminate after {} {}, stopping it forcefully", + ES_DESTROY_TIMEOUT, ES_DESTROY_TIMEOUT_UNIT); + processHandle.destroyForcibly(); + } - if (forcibly) { - processHandle.destroyForcibly(); - } else { - processHandle.destroy(); waitForProcessToExit(processHandle); - if (processHandle.isAlive() == false) { - return; + if (processHandle.isAlive()) { + throw new TestClustersException("Was not able to terminate elasticsearch process for " + this); } - LOGGER.info("process did not terminate after {} {}, stopping it forcefully", - ES_DESTROY_TIMEOUT, ES_DESTROY_TIMEOUT_UNIT); - processHandle.destroyForcibly(); - } - - waitForProcessToExit(processHandle); - if (processHandle.isAlive()) { - throw new TestClustersException("Was not able to terminate elasticsearch process for " + this); + } finally { + children.forEach(each -> stopHandle(each, forcibly)); } - - reaper.unregister(toString()); } private void logProcessInfo(String prefix, ProcessHandle.Info info) { diff --git a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/MlMappingsUpgradeIT.java b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/MlMappingsUpgradeIT.java index 8099812c8ea1b..13ed2dafc5f31 100644 --- a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/MlMappingsUpgradeIT.java +++ b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/MlMappingsUpgradeIT.java @@ -38,7 +38,6 @@ protected Collection templatesToWaitFor() { * The purpose of this test is to ensure that when a job is open through a rolling upgrade we upgrade the results * index mappings when it is assigned to an upgraded node even if no other ML endpoint is called after the upgrade */ - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/46262") public void testMappingsUpgrade() throws Exception { switch (CLUSTER_TYPE) { From 3e01df9f07ee2470087c9bc2a038e620dbd60af1 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Mon, 16 Dec 2019 07:43:42 +0100 Subject: [PATCH 208/686] "CONTAINS" support for BKD-backed geo_shape and shape fields (#50141) Lucene 8.4 added support for "CONTAINS", therefore in this commit those changes are integrated in Elasticsearch. This commit contains as well a bug fix when querying with a geometry collection with "DISJOINT" relation. --- .../mapping/types/geo-shape.asciidoc | 5 +- docs/reference/mapping/types/shape.asciidoc | 6 +- .../query-dsl/geo-shape-query.asciidoc | 3 +- docs/reference/query-dsl/shape-query.asciidoc | 8 +- .../common/geo/ShapeRelation.java | 1 + .../query/VectorGeoShapeQueryProcessor.java | 29 ++++-- .../search/geo/GeoShapeQueryTests.java | 99 ++++++++++++++++++- .../index/query/ShapeQueryProcessor.java | 31 ++++-- .../xpack/spatial/search/ShapeQueryTests.java | 95 ++++++++++++++++++ 9 files changed, 250 insertions(+), 27 deletions(-) diff --git a/docs/reference/mapping/types/geo-shape.asciidoc b/docs/reference/mapping/types/geo-shape.asciidoc index b39bf90609ae9..5ff464da9b995 100644 --- a/docs/reference/mapping/types/geo-shape.asciidoc +++ b/docs/reference/mapping/types/geo-shape.asciidoc @@ -142,9 +142,8 @@ The following features are not yet supported with the new indexing approach: using a `bool` query with each individual point. * `CONTAINS` relation query - when using the new default vector indexing strategy, `geo_shape` - queries with `relation` defined as `contains` are not yet supported. If this query relation - is an absolute necessity, it is recommended to set `strategy` to `quadtree` and use the - deprecated PrefixTree strategy indexing approach. + queries with `relation` defined as `contains` are supported for indices created with + ElasticSearch 7.5.0 or higher. [[prefix-trees]] [float] diff --git a/docs/reference/mapping/types/shape.asciidoc b/docs/reference/mapping/types/shape.asciidoc index 9874eb6cba525..3a180690ff44d 100644 --- a/docs/reference/mapping/types/shape.asciidoc +++ b/docs/reference/mapping/types/shape.asciidoc @@ -74,8 +74,8 @@ The following features are not yet supported: over each individual point. For now, if this is absolutely needed, this can be achieved using a `bool` query with each individual point. (Note: this could be very costly) -* `CONTAINS` relation query - `shape` queries with `relation` defined as `contains` are not - yet supported. +* `CONTAINS` relation query - `shape` queries with `relation` defined as `contains` are supported + for indices created with ElasticSearch 7.5.0 or higher. [float] ===== Example @@ -445,4 +445,4 @@ POST /example/_doc Due to the complex input structure and index representation of shapes, it is not currently possible to sort shapes or retrieve their fields directly. The `shape` value is only retrievable through the `_source` -field. \ No newline at end of file +field. diff --git a/docs/reference/query-dsl/geo-shape-query.asciidoc b/docs/reference/query-dsl/geo-shape-query.asciidoc index 35b56eb28e357..19a22ee103d91 100644 --- a/docs/reference/query-dsl/geo-shape-query.asciidoc +++ b/docs/reference/query-dsl/geo-shape-query.asciidoc @@ -151,8 +151,7 @@ has nothing in common with the query geometry. * `WITHIN` - Return all documents whose `geo_shape` field is within the query geometry. * `CONTAINS` - Return all documents whose `geo_shape` field -contains the query geometry. Note: this is only supported using the -`recursive` Prefix Tree Strategy deprecated[6.6] +contains the query geometry. [float] ==== Ignore Unmapped diff --git a/docs/reference/query-dsl/shape-query.asciidoc b/docs/reference/query-dsl/shape-query.asciidoc index a9850c3bf6814..8b58ec40329f7 100644 --- a/docs/reference/query-dsl/shape-query.asciidoc +++ b/docs/reference/query-dsl/shape-query.asciidoc @@ -170,12 +170,14 @@ GET /example/_search The following is a complete list of spatial relation operators available: -* `INTERSECTS` - (default) Return all documents whose `geo_shape` field +* `INTERSECTS` - (default) Return all documents whose `shape` field intersects the query geometry. -* `DISJOINT` - Return all documents whose `geo_shape` field +* `DISJOINT` - Return all documents whose `shape` field has nothing in common with the query geometry. -* `WITHIN` - Return all documents whose `geo_shape` field +* `WITHIN` - Return all documents whose `shape` field is within the query geometry. +* `CONTAINS` - Return all documents whose `shape` field +contains the query geometry. [float] ==== Ignore Unmapped diff --git a/server/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java b/server/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java index 23ba2f3ef6980..ac4481281a1c6 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java +++ b/server/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java @@ -69,6 +69,7 @@ public QueryRelation getLuceneRelation() { case INTERSECTS: return QueryRelation.INTERSECTS; case DISJOINT: return QueryRelation.DISJOINT; case WITHIN: return QueryRelation.WITHIN; + case CONTAINS: return QueryRelation.CONTAINS; default: throw new IllegalArgumentException("ShapeRelation [" + this + "] not supported"); } diff --git a/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java b/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java index 2f21037be33b4..adbdf269234f5 100644 --- a/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java +++ b/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java @@ -20,12 +20,14 @@ package org.elasticsearch.index.query; import org.apache.lucene.document.LatLonShape; +import org.apache.lucene.document.ShapeField; import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Polygon; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.Version; import org.elasticsearch.common.geo.GeoShapeType; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.geometry.Circle; @@ -49,10 +51,10 @@ public class VectorGeoShapeQueryProcessor implements AbstractGeometryFieldMapper @Override public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) { - // CONTAINS queries are not yet supported by VECTOR strategy - if (relation == ShapeRelation.CONTAINS) { + // CONTAINS queries are not supported by VECTOR strategy for indices created before version 7.5.0 (Lucene 8.3.0) + if (relation == ShapeRelation.CONTAINS && context.indexVersionCreated().before(Version.V_7_5_0)) { throw new QueryShardException(context, - ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]"); + ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]."); } // wrap geoQuery as a ConstantScoreQuery return getVectorQueryFromShape(shape, fieldName, relation, context); @@ -95,12 +97,21 @@ public Query visit(GeometryCollection collection) { } private void visit(BooleanQuery.Builder bqb, GeometryCollection collection) { + BooleanClause.Occur occur; + if (relation == ShapeRelation.CONTAINS || relation == ShapeRelation.DISJOINT) { + // all shapes must be disjoint / must be contained in relation to the indexed shape. + occur = BooleanClause.Occur.MUST; + } else { + // at least one shape must intersect / contain the indexed shape. + occur = BooleanClause.Occur.SHOULD; + } for (Geometry shape : collection) { if (shape instanceof MultiPoint) { - // Flatten multipoints + // Flatten multi-points + // We do not support multi-point queries? visit(bqb, (GeometryCollection) shape); } else { - bqb.add(shape.visit(this), BooleanClause.Occur.SHOULD); + bqb.add(shape.visit(this), occur); } } } @@ -144,7 +155,13 @@ public Query visit(MultiPolygon multiPolygon) { @Override public Query visit(Point point) { validateIsGeoShapeFieldType(); - return LatLonShape.newBoxQuery(fieldName, relation.getLuceneRelation(), + ShapeField.QueryRelation luceneRelation = relation.getLuceneRelation(); + if (luceneRelation == ShapeField.QueryRelation.CONTAINS) { + // contains and intersects are equivalent but the implementation of + // intersects is more efficient. + luceneRelation = ShapeField.QueryRelation.INTERSECTS; + } + return LatLonShape.newBoxQuery(fieldName, luceneRelation, point.getY(), point.getY(), point.getX(), point.getX()); } diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java index 021fd47350789..8ed913242ebda 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java @@ -447,10 +447,30 @@ public void testPointQuery() throws Exception { public void testContainsShapeQuery() throws Exception { // Create a random geometry collection. Rectangle mbr = xRandomRectangle(random(), xRandomPoint(random()), true); - GeometryCollectionBuilder gcb = createGeometryCollectionWithin(random(), mbr); + boolean usePrefixTrees = randomBoolean(); + GeometryCollectionBuilder gcb; + if (usePrefixTrees) { + gcb = createGeometryCollectionWithin(random(), mbr); + } else { + // vector strategy does not yet support multipoint queries + gcb = new GeometryCollectionBuilder(); + int numShapes = RandomNumbers.randomIntBetween(random(), 1, 4); + for (int i = 0; i < numShapes; ++i) { + ShapeBuilder shape; + do { + shape = RandomShapeGenerator.createShapeWithin(random(), mbr); + } while (shape instanceof MultiPointBuilder); + gcb.shape(shape); + } + } - client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape,tree=quadtree" ) - .get(); + if (usePrefixTrees) { + client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape,tree=quadtree") + .execute().actionGet(); + } else { + client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape") + .execute().actionGet(); + } XContentBuilder docSource = gcb.toXContent(jsonBuilder().startObject().field("location"), null).endObject(); client().prepareIndex("test").setId("1").setSource(docSource).setRefreshPolicy(IMMEDIATE).get(); @@ -727,4 +747,77 @@ public void testEnvelopeSpanningDateline() throws IOException { assertNotEquals("1", response.getHits().getAt(0).getId()); assertNotEquals("1", response.getHits().getAt(1).getId()); } + + public void testGeometryCollectionRelations() throws IOException { + XContentBuilder mapping = XContentFactory.jsonBuilder().startObject() + .startObject("doc") + .startObject("properties") + .startObject("geo").field("type", "geo_shape").endObject() + .endObject() + .endObject() + .endObject(); + + createIndex("test", Settings.builder().put("index.number_of_shards", 1).build(), "doc", mapping); + + EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10)); + + client().index(new IndexRequest("test") + .source(jsonBuilder().startObject().field("geo", envelopeBuilder).endObject()) + .setRefreshPolicy(IMMEDIATE)).actionGet(); + + { + // A geometry collection that is fully within the indexed shape + GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); + builder.shape(new PointBuilder(1, 2)); + builder.shape(new PointBuilder(-2, -1)); + SearchResponse response = client().prepareSearch("test") + .setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + response = client().prepareSearch("test") + .setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + response = client().prepareSearch("test") + .setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + } + // A geometry collection that is partially within the indexed shape + { + GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); + builder.shape(new PointBuilder(1, 2)); + builder.shape(new PointBuilder(20, 30)); + SearchResponse response = client().prepareSearch("test") + .setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + response = client().prepareSearch("test") + .setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + response = client().prepareSearch("test") + .setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + } + { + // A geometry collection that is disjoint with the indexed shape + GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); + builder.shape(new PointBuilder(-20, -30)); + builder.shape(new PointBuilder(20, 30)); + SearchResponse response = client().prepareSearch("test") + .setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + response = client().prepareSearch("test") + .setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + response = client().prepareSearch("test") + .setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + } + } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java index f95bc7e73d7ca..ee4cb2f5ba3ea 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.spatial.index.query; +import org.apache.lucene.document.ShapeField; import org.apache.lucene.document.XYShape; import org.apache.lucene.geo.XYLine; import org.apache.lucene.geo.XYPolygon; @@ -13,6 +14,7 @@ import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.Version; import org.elasticsearch.common.geo.GeoShapeType; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.geometry.Circle; @@ -38,14 +40,14 @@ public class ShapeQueryProcessor implements AbstractGeometryFieldMapper.QueryPro @Override public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) { - // CONTAINS queries are not yet supported by VECTOR strategy - if (relation == ShapeRelation.CONTAINS) { - throw new QueryShardException(context, - ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]"); - } if (shape == null) { return new MatchNoDocsQuery(); } + // CONTAINS queries are not supported by VECTOR strategy for indices created before version 7.5.0 (Lucene 8.3.0); + if (relation == ShapeRelation.CONTAINS && context.indexVersionCreated().before(Version.V_7_5_0)) { + throw new QueryShardException(context, + ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]."); + } // wrap geometry Query as a ConstantScoreQuery return new ConstantScoreQuery(shape.visit(new ShapeVisitor(context, fieldName, relation))); } @@ -76,12 +78,21 @@ public Query visit(GeometryCollection collection) { } private void visit(BooleanQuery.Builder bqb, GeometryCollection collection) { + BooleanClause.Occur occur; + if (relation == ShapeRelation.CONTAINS || relation == ShapeRelation.DISJOINT) { + // all shapes must be disjoint / must be contained in relation to the indexed shape. + occur = BooleanClause.Occur.MUST; + } else { + // at least one shape must intersect / contain the indexed shape. + occur = BooleanClause.Occur.SHOULD; + } for (Geometry shape : collection) { if (shape instanceof MultiPoint) { // Flatten multipoints + // We do not support multi-point queries? visit(bqb, (GeometryCollection) shape); } else { - bqb.add(shape.visit(this), BooleanClause.Occur.SHOULD); + bqb.add(shape.visit(this), occur); } } } @@ -128,7 +139,13 @@ private Query visitMultiPolygon(XYPolygon... polygons) { @Override public Query visit(Point point) { - return XYShape.newBoxQuery(fieldName, relation.getLuceneRelation(), + ShapeField.QueryRelation luceneRelation = relation.getLuceneRelation(); + if (luceneRelation == ShapeField.QueryRelation.CONTAINS) { + // contains and intersects are equivalent but the implementation of + // intersects is more efficient. + luceneRelation = ShapeField.QueryRelation.INTERSECTS; + } + return XYShape.newBoxQuery(fieldName, luceneRelation, (float)point.getX(), (float)point.getX(), (float)point.getY(), (float)point.getY()); } diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryTests.java index 682b377c50355..1d11c54b69e47 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryTests.java @@ -6,10 +6,14 @@ package org.elasticsearch.xpack.spatial.search; import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.geo.GeoJson; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.builders.EnvelopeBuilder; +import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; +import org.elasticsearch.common.geo.builders.PointBuilder; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; @@ -25,6 +29,7 @@ import org.elasticsearch.xpack.spatial.util.ShapeTestUtils; import org.locationtech.jts.geom.Coordinate; +import java.io.IOException; import java.util.Collection; import java.util.Locale; @@ -239,4 +244,94 @@ public void testFieldAlias() { .get(); assertTrue(response.getHits().getTotalHits().value > 0); } + + public void testContainsShapeQuery() { + + client().admin().indices().prepareCreate("test_contains").addMapping("type", "location", "type=shape") + .execute().actionGet(); + + String doc = "{\"location\" : {\"type\":\"envelope\", \"coordinates\":[ [-100.0, 100.0], [100.0, -100.0]]}}"; + client().prepareIndex("test_contains").setId("1").setSource(doc, XContentType.JSON).setRefreshPolicy(IMMEDIATE).get(); + + // index the mbr of the collection + EnvelopeBuilder queryShape = new EnvelopeBuilder(new Coordinate(-50, 50), new Coordinate(50, -50)); + ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder("location", queryShape.buildGeometry()).relation(ShapeRelation.CONTAINS); + SearchResponse response = client().prepareSearch("test_contains").setQuery(queryBuilder).get(); + assertSearchResponse(response); + + assertThat(response.getHits().getTotalHits().value, equalTo(1L)); + } + + public void testGeometryCollectionRelations() throws IOException { + XContentBuilder mapping = XContentFactory.jsonBuilder().startObject() + .startObject("doc") + .startObject("properties") + .startObject("geometry").field("type", "shape").endObject() + .endObject() + .endObject() + .endObject(); + + createIndex("test_collections", Settings.builder().put("index.number_of_shards", 1).build(), "doc", mapping); + + EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10)); + + client().index(new IndexRequest("test_collections") + .source(jsonBuilder().startObject().field("geometry", envelopeBuilder).endObject()) + .setRefreshPolicy(IMMEDIATE)).actionGet(); + + { + // A geometry collection that is fully within the indexed shape + GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); + builder.shape(new PointBuilder(1, 2)); + builder.shape(new PointBuilder(-2, -1)); + SearchResponse response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + } + { + // A geometry collection that is partially within the indexed shape + GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); + builder.shape(new PointBuilder(1, 2)); + builder.shape(new PointBuilder(20, 30)); + SearchResponse response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + } + { + // A geometry collection that is disjoint with the indexed shape + GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); + builder.shape(new PointBuilder(-20, -30)); + builder.shape(new PointBuilder(20, 30)); + SearchResponse response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + } + } } From 9114051a2442a21150520cc746c9e8639ea25216 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 16 Dec 2019 10:39:26 +0100 Subject: [PATCH 209/686] Remove BlobContainer Tests against Mocks (#50194) * Remove BlobContainer Tests against Mocks Removing all these weird mocks as asked for by #30424. All these tests are now part of real repository ITs and otherwise left unchanged if they had independent tests that didn't call the `createBlobStore` method previously. The HDFS tests also get added coverage as a side-effect because they did not have an implementation of the abstract repository ITs. Closes #30424 --- .../azure/AzureBlobContainerRetriesTests.java | 2 +- .../azure/AzureBlobStoreContainerTests.java | 53 -- .../azure/AzureStorageServiceMock.java | 167 ----- .../cloud/storage/StorageRpcOptionUtils.java | 43 -- .../cloud/storage/StorageTestUtils.java | 37 -- ...CloudStorageBlobContainerRetriesTests.java | 2 +- ...leCloudStorageBlobStoreContainerTests.java | 47 +- ...eCloudStorageBlobStoreRepositoryTests.java | 33 + .../repositories/gcs/MockStorage.java | 574 ------------------ .../hdfs/HdfsBlobStoreContainerTests.java | 16 +- .../hdfs/HdfsBlobStoreRepositoryTests.java | 55 ++ .../hdfs/HdfsRepositoryTests.java | 2 +- .../repositories/s3/MockAmazonS3.java | 169 ------ .../s3/S3BlobStoreContainerTests.java | 50 +- .../fs/FsBlobStoreContainerTests.java | 81 --- .../fs/FsBlobStoreRepositoryIT.java | 41 +- .../ESBlobStoreContainerTestCase.java | 213 ------- .../ESBlobStoreRepositoryIntegTestCase.java | 172 ++++++ .../security/authc/TokenServiceTests.java | 2 +- 19 files changed, 314 insertions(+), 1445 deletions(-) delete mode 100644 plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobStoreContainerTests.java delete mode 100644 plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureStorageServiceMock.java delete mode 100644 plugins/repository-gcs/src/test/java/com/google/cloud/storage/StorageRpcOptionUtils.java delete mode 100644 plugins/repository-gcs/src/test/java/com/google/cloud/storage/StorageTestUtils.java delete mode 100644 plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/MockStorage.java create mode 100644 plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreRepositoryTests.java delete mode 100644 plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/MockAmazonS3.java delete mode 100644 server/src/test/java/org/elasticsearch/common/blobstore/fs/FsBlobStoreContainerTests.java delete mode 100644 test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java diff --git a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobContainerRetriesTests.java b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobContainerRetriesTests.java index 1c069ca189951..ce3cba065c35b 100644 --- a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobContainerRetriesTests.java +++ b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobContainerRetriesTests.java @@ -72,13 +72,13 @@ import java.util.stream.Collectors; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.elasticsearch.repositories.ESBlobStoreContainerTestCase.randomBytes; import static org.elasticsearch.repositories.azure.AzureRepository.Repository.CONTAINER_SETTING; import static org.elasticsearch.repositories.azure.AzureStorageSettings.ACCOUNT_SETTING; import static org.elasticsearch.repositories.azure.AzureStorageSettings.ENDPOINT_SUFFIX_SETTING; import static org.elasticsearch.repositories.azure.AzureStorageSettings.KEY_SETTING; import static org.elasticsearch.repositories.azure.AzureStorageSettings.MAX_RETRIES_SETTING; import static org.elasticsearch.repositories.azure.AzureStorageSettings.TIMEOUT_SETTING; +import static org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase.randomBytes; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; diff --git a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobStoreContainerTests.java b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobStoreContainerTests.java deleted file mode 100644 index 07d0a1e18d3bd..0000000000000 --- a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobStoreContainerTests.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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. - */ - -package org.elasticsearch.repositories.azure; - -import org.elasticsearch.cluster.metadata.RepositoryMetaData; -import org.elasticsearch.common.blobstore.BlobStore; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.repositories.ESBlobStoreContainerTestCase; -import org.elasticsearch.threadpool.TestThreadPool; -import org.elasticsearch.threadpool.ThreadPool; - -import java.util.concurrent.TimeUnit; - -public class AzureBlobStoreContainerTests extends ESBlobStoreContainerTestCase { - - private ThreadPool threadPool; - - @Override - public void setUp() throws Exception { - super.setUp(); - threadPool = new TestThreadPool("AzureBlobStoreTests", AzureRepositoryPlugin.executorBuilder()); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - ThreadPool.terminate(threadPool, 10L, TimeUnit.SECONDS); - } - - @Override - protected BlobStore newBlobStore() { - RepositoryMetaData repositoryMetaData = new RepositoryMetaData("azure", "ittest", Settings.EMPTY); - AzureStorageServiceMock client = new AzureStorageServiceMock(); - return new AzureBlobStore(repositoryMetaData, client, threadPool); - } -} diff --git a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureStorageServiceMock.java b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureStorageServiceMock.java deleted file mode 100644 index 2217b6743fb2e..0000000000000 --- a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureStorageServiceMock.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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. - */ - -package org.elasticsearch.repositories.azure; - -import com.microsoft.azure.storage.OperationContext; -import com.microsoft.azure.storage.StorageException; -import com.microsoft.azure.storage.blob.CloudBlobClient; -import org.elasticsearch.common.blobstore.BlobMetaData; -import org.elasticsearch.common.blobstore.support.PlainBlobMetaData; -import org.elasticsearch.common.collect.Tuple; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.internal.io.Streams; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.SocketPermission; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.NoSuchFileException; -import java.security.AccessController; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; - -import static java.util.Collections.emptyMap; - -/** - * In memory storage for unit tests - */ -public class AzureStorageServiceMock extends AzureStorageService { - - protected final Map blobs = new ConcurrentHashMap<>(); - - public AzureStorageServiceMock() { - super(Settings.EMPTY); - } - - @Override - public boolean blobExists(String account, String container, String blob) { - return blobs.containsKey(blob); - } - - @Override - public void deleteBlob(String account, String container, String blob) throws StorageException { - if (blobs.remove(blob) == null) { - throw new StorageException("BlobNotFound", "[" + blob + "] does not exist.", 404, null, null); - } - } - - @Override - public InputStream getInputStream(String account, String container, String blob) throws IOException { - if (!blobExists(account, container, blob)) { - throw new NoSuchFileException("missing blob [" + blob + "]"); - } - return AzureStorageService.giveSocketPermissionsToStream(new PermissionRequiringInputStream(blobs.get(blob).toByteArray())); - } - - @Override - public Map listBlobsByPrefix(String account, String container, String keyPath, String prefix) { - final var blobsBuilder = new HashMap(); - blobs.forEach((String blobName, ByteArrayOutputStream bos) -> { - final String checkBlob; - if (keyPath != null && !keyPath.isEmpty()) { - // strip off key path from the beginning of the blob name - checkBlob = blobName.replace(keyPath, ""); - } else { - checkBlob = blobName; - } - if (prefix == null || startsWithIgnoreCase(checkBlob, prefix)) { - blobsBuilder.put(blobName, new PlainBlobMetaData(checkBlob, bos.size())); - } - }); - return Map.copyOf(blobsBuilder); - } - - @Override - public void writeBlob(String account, String container, String blobName, InputStream inputStream, long blobSize, - boolean failIfAlreadyExists) throws StorageException, FileAlreadyExistsException { - if (failIfAlreadyExists && blobs.containsKey(blobName)) { - throw new FileAlreadyExistsException(blobName); - } - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - blobs.put(blobName, outputStream); - Streams.copy(inputStream, outputStream); - } catch (IOException e) { - throw new StorageException("MOCK", "Error while writing mock stream", e); - } - } - - /** - * Test if the given String starts with the specified prefix, - * ignoring upper/lower case. - * - * @param str the String to check - * @param prefix the prefix to look for - * @see java.lang.String#startsWith - */ - private static boolean startsWithIgnoreCase(String str, String prefix) { - if (str == null || prefix == null) { - return false; - } - if (str.startsWith(prefix)) { - return true; - } - if (str.length() < prefix.length()) { - return false; - } - String lcStr = str.substring(0, prefix.length()).toLowerCase(Locale.ROOT); - String lcPrefix = prefix.toLowerCase(Locale.ROOT); - return lcStr.equals(lcPrefix); - } - - private static class PermissionRequiringInputStream extends ByteArrayInputStream { - - private PermissionRequiringInputStream(byte[] buf) { - super(buf); - } - - @Override - public synchronized int read() { - AccessController.checkPermission(new SocketPermission("*", "connect")); - return super.read(); - } - - @Override - public int read(byte[] b) throws IOException { - AccessController.checkPermission(new SocketPermission("*", "connect")); - return super.read(b); - } - - @Override - public synchronized int read(byte[] b, int off, int len) { - AccessController.checkPermission(new SocketPermission("*", "connect")); - return super.read(b, off, len); - } - } - - @Override - public Tuple> client(String clientName) { - return null; - } - - @Override - public Map refreshAndClearCache(Map clientsSettings) { - return emptyMap(); - } -} diff --git a/plugins/repository-gcs/src/test/java/com/google/cloud/storage/StorageRpcOptionUtils.java b/plugins/repository-gcs/src/test/java/com/google/cloud/storage/StorageRpcOptionUtils.java deleted file mode 100644 index a08ae2f9f8a8a..0000000000000 --- a/plugins/repository-gcs/src/test/java/com/google/cloud/storage/StorageRpcOptionUtils.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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. - */ - -package com.google.cloud.storage; - -import com.google.cloud.storage.spi.v1.StorageRpc; - -/** - * Utility class that exposed Google SDK package protected methods to - * create specific StorageRpc objects in unit tests. - */ -public class StorageRpcOptionUtils { - - private StorageRpcOptionUtils(){} - - public static String getPrefix(final Storage.BlobListOption... options) { - if (options != null) { - for (final Option option : options) { - final StorageRpc.Option rpcOption = option.getRpcOption(); - if (StorageRpc.Option.PREFIX.equals(rpcOption)) { - return (String) option.getValue(); - } - } - } - return null; - } -} diff --git a/plugins/repository-gcs/src/test/java/com/google/cloud/storage/StorageTestUtils.java b/plugins/repository-gcs/src/test/java/com/google/cloud/storage/StorageTestUtils.java deleted file mode 100644 index 68175d7f1be53..0000000000000 --- a/plugins/repository-gcs/src/test/java/com/google/cloud/storage/StorageTestUtils.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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. - */ - -package com.google.cloud.storage; - -/** - * Utility class that exposed Google SDK package protected methods to - * create buckets and blobs objects in unit tests. - */ -public class StorageTestUtils { - - private StorageTestUtils(){} - - public static Bucket createBucket(final Storage storage, final String bucketName) { - return new Bucket(storage, (BucketInfo.BuilderImpl) BucketInfo.newBuilder(bucketName)); - } - - public static Blob createBlob(final Storage storage, final String bucketName, final String blobName, final long blobSize) { - return new Blob(storage, (BlobInfo.BuilderImpl) BlobInfo.newBuilder(bucketName, blobName).setSize(blobSize)); - } -} diff --git a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java index 00dbf758422f7..23203e8d4a9e0 100644 --- a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java +++ b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java @@ -74,7 +74,7 @@ import static fixture.gcs.GoogleCloudStorageHttpHandler.getContentRangeStart; import static fixture.gcs.GoogleCloudStorageHttpHandler.parseMultipartRequestBody; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.elasticsearch.repositories.ESBlobStoreContainerTestCase.randomBytes; +import static org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase.randomBytes; import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.CREDENTIALS_FILE_SETTING; import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.ENDPOINT_SETTING; import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.READ_TIMEOUT_SETTING; diff --git a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreContainerTests.java b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreContainerTests.java index 311544160ad73..de75e5b825570 100644 --- a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreContainerTests.java +++ b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreContainerTests.java @@ -26,20 +26,14 @@ import com.google.cloud.storage.StorageBatch; import com.google.cloud.storage.StorageBatchResult; import com.google.cloud.storage.StorageException; -import org.apache.lucene.util.BytesRef; -import org.apache.lucene.util.BytesRefBuilder; import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.repositories.ESBlobStoreContainerTestCase; +import org.elasticsearch.test.ESTestCase; import java.io.IOException; -import java.io.InputStream; import java.util.Arrays; import java.util.List; -import java.util.Locale; -import java.util.concurrent.ConcurrentHashMap; import static org.hamcrest.Matchers.instanceOf; import static org.mockito.Matchers.any; @@ -51,44 +45,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class GoogleCloudStorageBlobStoreContainerTests extends ESBlobStoreContainerTestCase { - - @Override - protected BlobStore newBlobStore() { - final String bucketName = randomAlphaOfLength(randomIntBetween(1, 10)).toLowerCase(Locale.ROOT); - final String clientName = randomAlphaOfLength(randomIntBetween(1, 10)).toLowerCase(Locale.ROOT); - final GoogleCloudStorageService storageService = mock(GoogleCloudStorageService.class); - try { - when(storageService.client(any(String.class))).thenReturn(new MockStorage(bucketName, new ConcurrentHashMap<>(), random())); - } catch (final Exception e) { - throw new RuntimeException(e); - } - return new GoogleCloudStorageBlobStore(bucketName, clientName, storageService); - } - - public void testWriteReadLarge() throws IOException { - try(BlobStore store = newBlobStore()) { - final BlobContainer container = store.blobContainer(new BlobPath()); - byte[] data = randomBytes(GoogleCloudStorageBlobStore.LARGE_BLOB_THRESHOLD_BYTE_SIZE + 1); - writeBlob(container, "foobar", new BytesArray(data), randomBoolean()); - if (randomBoolean()) { - // override file, to check if we get latest contents - random().nextBytes(data); - writeBlob(container, "foobar", new BytesArray(data), false); - } - try (InputStream stream = container.readBlob("foobar")) { - BytesRefBuilder target = new BytesRefBuilder(); - while (target.length() < data.length) { - byte[] buffer = new byte[scaledRandomIntBetween(1, data.length - target.length())]; - int offset = scaledRandomIntBetween(0, buffer.length - 1); - int read = stream.read(buffer, offset, buffer.length - offset); - target.append(new BytesRef(buffer, offset, read)); - } - assertEquals(data.length, target.length()); - assertArrayEquals(data, Arrays.copyOfRange(target.bytes(), 0, target.length())); - } - } - } +public class GoogleCloudStorageBlobStoreContainerTests extends ESTestCase { @SuppressWarnings("unchecked") public void testDeleteBlobsIgnoringIfNotExistsThrowsIOException() throws Exception { diff --git a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java index d8926b25e2c49..0333d96510397 100644 --- a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java +++ b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java @@ -26,11 +26,17 @@ import com.sun.net.httpserver.HttpHandler; import fixture.gcs.FakeOAuth2HttpHandler; import fixture.gcs.GoogleCloudStorageHttpHandler; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefBuilder; import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.metadata.RepositoryMetaData; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.blobstore.BlobContainer; +import org.elasticsearch.common.blobstore.BlobPath; +import org.elasticsearch.common.blobstore.BlobStore; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.Settings; @@ -46,6 +52,8 @@ import org.threeten.bp.Duration; import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Map; @@ -152,6 +160,31 @@ public void testChunkSize() { assertEquals("failed to parse value [101mb] for setting [chunk_size], must be <= [100mb]", e.getMessage()); } + public void testWriteReadLarge() throws IOException { + try (BlobStore store = newBlobStore()) { + final BlobContainer container = store.blobContainer(new BlobPath()); + byte[] data = randomBytes(GoogleCloudStorageBlobStore.LARGE_BLOB_THRESHOLD_BYTE_SIZE + 1); + writeBlob(container, "foobar", new BytesArray(data), randomBoolean()); + if (randomBoolean()) { + // override file, to check if we get latest contents + random().nextBytes(data); + writeBlob(container, "foobar", new BytesArray(data), false); + } + try (InputStream stream = container.readBlob("foobar")) { + BytesRefBuilder target = new BytesRefBuilder(); + while (target.length() < data.length) { + byte[] buffer = new byte[scaledRandomIntBetween(1, data.length - target.length())]; + int offset = scaledRandomIntBetween(0, buffer.length - 1); + int read = stream.read(buffer, offset, buffer.length - offset); + target.append(new BytesRef(buffer, offset, read)); + } + assertEquals(data.length, target.length()); + assertArrayEquals(data, Arrays.copyOfRange(target.bytes(), 0, target.length())); + } + container.delete(); + } + } + public static class TestGoogleCloudStoragePlugin extends GoogleCloudStoragePlugin { public TestGoogleCloudStoragePlugin(Settings settings) { diff --git a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/MockStorage.java b/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/MockStorage.java deleted file mode 100644 index 627bb8de94300..0000000000000 --- a/plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/MockStorage.java +++ /dev/null @@ -1,574 +0,0 @@ -/* - * 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. - */ - -package org.elasticsearch.repositories.gcs; - -import com.google.api.gax.paging.Page; -import com.google.cloud.BatchResult; -import com.google.cloud.Policy; -import com.google.cloud.ReadChannel; -import com.google.cloud.RestorableState; -import com.google.cloud.WriteChannel; -import com.google.cloud.storage.Acl; -import com.google.cloud.storage.Blob; -import com.google.cloud.storage.BlobId; -import com.google.cloud.storage.BlobInfo; -import com.google.cloud.storage.Bucket; -import com.google.cloud.storage.BucketInfo; -import com.google.cloud.storage.CopyWriter; -import com.google.cloud.storage.ServiceAccount; -import com.google.cloud.storage.Storage; -import com.google.cloud.storage.StorageBatch; -import com.google.cloud.storage.StorageBatchResult; -import com.google.cloud.storage.StorageException; -import com.google.cloud.storage.StorageOptions; -import com.google.cloud.storage.StorageRpcOptionUtils; -import com.google.cloud.storage.StorageTestUtils; -import org.elasticsearch.common.util.concurrent.ConcurrentCollections; -import org.elasticsearch.core.internal.io.IOUtils; -import org.mockito.stubbing.Answer; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyVararg; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; - -/** - * {@link MockStorage} mocks a {@link Storage} client by storing all the blobs - * in a given concurrent map. - */ -class MockStorage implements Storage { - - private final Random random; - private final String bucketName; - private final ConcurrentMap blobs; - - MockStorage(final String bucket, final ConcurrentMap blobs, final Random random) { - this.random = random; - this.bucketName = Objects.requireNonNull(bucket); - this.blobs = Objects.requireNonNull(blobs); - } - - @Override - public Bucket get(String bucket, BucketGetOption... options) { - if (bucketName.equals(bucket)) { - return StorageTestUtils.createBucket(this, bucketName); - } else { - return null; - } - } - - @Override - public Bucket lockRetentionPolicy(final BucketInfo bucket, final BucketTargetOption... options) { - return null; - } - - @Override - public Blob get(BlobId blob) { - if (bucketName.equals(blob.getBucket())) { - final byte[] bytes = blobs.get(blob.getName()); - if (bytes != null) { - return StorageTestUtils.createBlob(this, bucketName, blob.getName(), bytes.length); - } - } - return null; - } - - @Override - public boolean delete(BlobId blob) { - if (bucketName.equals(blob.getBucket()) && blobs.containsKey(blob.getName())) { - return blobs.remove(blob.getName()) != null; - } - return false; - } - - @Override - public List delete(Iterable blobIds) { - final List ans = new ArrayList<>(); - for (final BlobId blobId : blobIds) { - ans.add(delete(blobId)); - } - return ans; - } - - @Override - public Blob create(BlobInfo blobInfo, byte[] content, BlobTargetOption... options) { - if (bucketName.equals(blobInfo.getBucket()) == false) { - throw new StorageException(404, "Bucket not found"); - } - if (Stream.of(options).anyMatch(option -> option.equals(BlobTargetOption.doesNotExist()))) { - byte[] existingBytes = blobs.putIfAbsent(blobInfo.getName(), content); - if (existingBytes != null) { - throw new StorageException(412, "Blob already exists"); - } - } else { - blobs.put(blobInfo.getName(), content); - } - return get(BlobId.of(blobInfo.getBucket(), blobInfo.getName())); - } - - @Override - public Page list(String bucket, BlobListOption... options) { - if (bucketName.equals(bucket) == false) { - throw new StorageException(404, "Bucket not found"); - } - final Storage storage = this; - final String prefix = StorageRpcOptionUtils.getPrefix(options); - - return new Page() { - @Override - public boolean hasNextPage() { - return false; - } - - @Override - public String getNextPageToken() { - return null; - } - - @Override - public Page getNextPage() { - throw new UnsupportedOperationException(); - } - - @Override - public Iterable iterateAll() { - return blobs.entrySet().stream() - .filter(blob -> ((prefix == null) || blob.getKey().startsWith(prefix))) - .map(blob -> StorageTestUtils.createBlob(storage, bucketName, blob.getKey(), blob.getValue().length)) - .collect(Collectors.toList()); - } - - @Override - public Iterable getValues() { - throw new UnsupportedOperationException(); - } - }; - } - - @Override - public ReadChannel reader(BlobId blob, BlobSourceOption... options) { - if (bucketName.equals(blob.getBucket())) { - final byte[] bytes = blobs.get(blob.getName()); - - final ReadableByteChannel readableByteChannel; - if (bytes != null) { - readableByteChannel = Channels.newChannel(new ByteArrayInputStream(bytes)); - } else { - readableByteChannel = new ReadableByteChannel() { - @Override - public int read(ByteBuffer dst) throws IOException { - throw new StorageException(404, "Object not found"); - } - - @Override - public boolean isOpen() { - return false; - } - - @Override - public void close() throws IOException { - } - }; - } - return new ReadChannel() { - @Override - public void close() { - IOUtils.closeWhileHandlingException(readableByteChannel); - } - - @Override - public void seek(long position) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public void setChunkSize(int chunkSize) { - throw new UnsupportedOperationException(); - } - - @Override - public RestorableState capture() { - throw new UnsupportedOperationException(); - } - - @Override - public int read(ByteBuffer dst) throws IOException { - return readableByteChannel.read(dst); - } - - @Override - public boolean isOpen() { - return readableByteChannel.isOpen(); - } - }; - } - return null; - } - - private final Set simulated410s = ConcurrentCollections.newConcurrentSet(); - - @Override - public WriteChannel writer(BlobInfo blobInfo, BlobWriteOption... options) { - if (bucketName.equals(blobInfo.getBucket())) { - final ByteArrayOutputStream output = new ByteArrayOutputStream(); - return new WriteChannel() { - - private volatile boolean failed; - - final WritableByteChannel writableByteChannel = Channels.newChannel(output); - - @Override - public void setChunkSize(int chunkSize) { - throw new UnsupportedOperationException(); - } - - @Override - public RestorableState capture() { - throw new UnsupportedOperationException(); - } - - @Override - public int write(ByteBuffer src) throws IOException { - // Only fail a blob once on a 410 error since the error is so unlikely in practice - if (simulated410s.add(blobInfo) && random.nextBoolean()) { - failed = true; - throw new StorageException(HttpURLConnection.HTTP_GONE, "Simulated lost resumeable upload session"); - } - return writableByteChannel.write(src); - } - - @Override - public boolean isOpen() { - return writableByteChannel.isOpen(); - } - - @Override - public void close() { - IOUtils.closeWhileHandlingException(writableByteChannel); - if (failed == false) { - if (Stream.of(options).anyMatch(option -> option.equals(BlobWriteOption.doesNotExist()))) { - byte[] existingBytes = blobs.putIfAbsent(blobInfo.getName(), output.toByteArray()); - if (existingBytes != null) { - throw new StorageException(412, "Blob already exists"); - } - } else { - blobs.put(blobInfo.getName(), output.toByteArray()); - } - } - } - }; - } - return null; - } - - @Override - public WriteChannel writer(URL signedURL) { - return null; - } - - // Everything below this line is not implemented. - - @Override - public CopyWriter copy(CopyRequest copyRequest) { - return null; - } - - @Override - public Blob create(BlobInfo blobInfo, byte[] content, int offset, int length, BlobTargetOption... options) { - return null; - } - - @Override - public Bucket create(BucketInfo bucketInfo, BucketTargetOption... options) { - return null; - } - - @Override - public Blob create(BlobInfo blobInfo, BlobTargetOption... options) { - return null; - } - - @Override - public Blob create(BlobInfo blobInfo, InputStream content, BlobWriteOption... options) { - return null; - } - - @Override - public Blob get(String bucket, String blob, BlobGetOption... options) { - return null; - } - - @Override - public Blob get(BlobId blob, BlobGetOption... options) { - return null; - } - - @Override - public Page list(BucketListOption... options) { - return null; - } - - @Override - public Bucket update(BucketInfo bucketInfo, BucketTargetOption... options) { - return null; - } - - @Override - public Blob update(BlobInfo blobInfo, BlobTargetOption... options) { - return null; - } - - @Override - public Blob update(BlobInfo blobInfo) { - return null; - } - - @Override - public boolean delete(String bucket, BucketSourceOption... options) { - return false; - } - - @Override - public boolean delete(String bucket, String blob, BlobSourceOption... options) { - return false; - } - - @Override - public boolean delete(BlobId blob, BlobSourceOption... options) { - return false; - } - - @Override - public Blob compose(ComposeRequest composeRequest) { - return null; - } - - @Override - public byte[] readAllBytes(String bucket, String blob, BlobSourceOption... options) { - return new byte[0]; - } - - @Override - public byte[] readAllBytes(BlobId blob, BlobSourceOption... options) { - return new byte[0]; - } - - @Override - @SuppressWarnings("unchecked") - public StorageBatch batch() { - final Answer throwOnMissingMock = invocationOnMock -> { - throw new AssertionError("Did not expect call to method [" + invocationOnMock.getMethod().getName() + ']'); - }; - final StorageBatch batch = mock(StorageBatch.class, throwOnMissingMock); - StorageBatchResult result = mock(StorageBatchResult.class, throwOnMissingMock); - doAnswer(answer -> { - BatchResult.Callback callback = (BatchResult.Callback) answer.getArguments()[0]; - callback.success(true); - return null; - }).when(result).notify(any(BatchResult.Callback.class)); - doAnswer(invocation -> { - final BlobId blobId = (BlobId) invocation.getArguments()[0]; - delete(blobId); - return result; - }).when(batch).delete(any(BlobId.class), anyVararg()); - doAnswer(invocation -> null).when(batch).submit(); - return batch; - } - - @Override - public ReadChannel reader(String bucket, String blob, BlobSourceOption... options) { - return null; - } - - @Override - public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options) { - return null; - } - - @Override - public List get(BlobId... blobIds) { - return null; - } - - @Override - public List get(Iterable blobIds) { - return null; - } - - @Override - public List update(BlobInfo... blobInfos) { - return null; - } - - @Override - public List update(Iterable blobInfos) { - return null; - } - - @Override - public List delete(BlobId... blobIds) { - return null; - } - - @Override - public Acl getAcl(String bucket, Acl.Entity entity, BucketSourceOption... options) { - return null; - } - - @Override - public Acl getAcl(String bucket, Acl.Entity entity) { - return null; - } - - @Override - public boolean deleteAcl(String bucket, Acl.Entity entity, BucketSourceOption... options) { - return false; - } - - @Override - public boolean deleteAcl(String bucket, Acl.Entity entity) { - return false; - } - - @Override - public Acl createAcl(String bucket, Acl acl, BucketSourceOption... options) { - return null; - } - - @Override - public Acl createAcl(String bucket, Acl acl) { - return null; - } - - @Override - public Acl updateAcl(String bucket, Acl acl, BucketSourceOption... options) { - return null; - } - - @Override - public Acl updateAcl(String bucket, Acl acl) { - return null; - } - - @Override - public List listAcls(String bucket, BucketSourceOption... options) { - return null; - } - - @Override - public List listAcls(String bucket) { - return null; - } - - @Override - public Acl getDefaultAcl(String bucket, Acl.Entity entity) { - return null; - } - - @Override - public boolean deleteDefaultAcl(String bucket, Acl.Entity entity) { - return false; - } - - @Override - public Acl createDefaultAcl(String bucket, Acl acl) { - return null; - } - - @Override - public Acl updateDefaultAcl(String bucket, Acl acl) { - return null; - } - - @Override - public List listDefaultAcls(String bucket) { - return null; - } - - @Override - public Acl getAcl(BlobId blob, Acl.Entity entity) { - return null; - } - - @Override - public boolean deleteAcl(BlobId blob, Acl.Entity entity) { - return false; - } - - @Override - public Acl createAcl(BlobId blob, Acl acl) { - return null; - } - - @Override - public Acl updateAcl(BlobId blob, Acl acl) { - return null; - } - - @Override - public List listAcls(BlobId blob) { - return null; - } - - @Override - public Policy getIamPolicy(String bucket, BucketSourceOption... options) { - return null; - } - - @Override - public Policy setIamPolicy(String bucket, Policy policy, BucketSourceOption... options) { - return null; - } - - @Override - public List testIamPermissions(String bucket, List permissions, BucketSourceOption... options) { - return null; - } - - @Override - public ServiceAccount getServiceAccount(String projectId) { - return null; - } - - @Override - public StorageOptions getOptions() { - return null; - } -} diff --git a/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreContainerTests.java b/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreContainerTests.java index 83e5c581e065c..b2c71c0031a62 100644 --- a/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreContainerTests.java +++ b/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreContainerTests.java @@ -28,13 +28,12 @@ import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobPath; -import org.elasticsearch.common.blobstore.BlobStore; import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.repositories.ESBlobStoreContainerTestCase; import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; +import org.elasticsearch.test.ESTestCase; import javax.security.auth.Subject; -import java.io.IOException; + import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.net.URI; @@ -45,13 +44,12 @@ import java.security.PrivilegedExceptionAction; import java.util.Collections; -@ThreadLeakFilters(filters = {HdfsClientThreadLeakFilter.class}) -public class HdfsBlobStoreContainerTests extends ESBlobStoreContainerTestCase { +import static org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase.randomBytes; +import static org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase.readBlobFully; +import static org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase.writeBlob; - @Override - protected BlobStore newBlobStore() throws IOException { - return new HdfsBlobStore(createTestContext(), "temp", 1024, false); - } +@ThreadLeakFilters(filters = {HdfsClientThreadLeakFilter.class}) +public class HdfsBlobStoreContainerTests extends ESTestCase { private FileContext createTestContext() { FileContext fileContext; diff --git a/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreRepositoryTests.java b/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreRepositoryTests.java new file mode 100644 index 0000000000000..4b4a315da8813 --- /dev/null +++ b/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreRepositoryTests.java @@ -0,0 +1,55 @@ +/* + * 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. + */ +package org.elasticsearch.repositories.hdfs; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase; +import org.elasticsearch.test.ESIntegTestCase; + +import java.util.Collection; +import java.util.Collections; + +@ThreadLeakFilters(filters = HdfsClientThreadLeakFilter.class) +// Ony using a single node here since the TestingFs only supports the single-node case +@ESIntegTestCase.ClusterScope(numDataNodes = 1, supportsDedicatedMasters = false) +public class HdfsBlobStoreRepositoryTests extends ESBlobStoreRepositoryIntegTestCase { + + @Override + protected String repositoryType() { + return "hdfs"; + } + + @Override + protected Settings repositorySettings() { + assumeFalse("https://github.com/elastic/elasticsearch/issues/31498", HdfsRepositoryTests.isJava11()); + return Settings.builder() + .put("uri", "hdfs:///") + .put("conf.fs.AbstractFileSystem.hdfs.impl", TestingFs.class.getName()) + .put("path", "foo") + .put("chunk_size", randomIntBetween(100, 1000) + "k") + .put("compress", randomBoolean()).build(); + } + + @Override + protected Collection> nodePlugins() { + return Collections.singletonList(HdfsPlugin.class); + } +} diff --git a/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsRepositoryTests.java b/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsRepositoryTests.java index 63496c00db843..05c339293ac7c 100644 --- a/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsRepositoryTests.java +++ b/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsRepositoryTests.java @@ -78,7 +78,7 @@ protected void assertCleanupResponse(CleanupRepositoryResponse response, long by } } - private static boolean isJava11() { + public static boolean isJava11() { return JavaVersion.current().equals(JavaVersion.parse("11")); } } diff --git a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/MockAmazonS3.java b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/MockAmazonS3.java deleted file mode 100644 index 37f5d9b03dbc2..0000000000000 --- a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/MockAmazonS3.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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. - */ - -package org.elasticsearch.repositories.s3; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.SdkClientException; -import com.amazonaws.services.s3.AbstractAmazonS3; -import com.amazonaws.services.s3.model.AmazonS3Exception; -import com.amazonaws.services.s3.model.DeleteObjectRequest; -import com.amazonaws.services.s3.model.DeleteObjectsRequest; -import com.amazonaws.services.s3.model.DeleteObjectsResult; -import com.amazonaws.services.s3.model.GetObjectRequest; -import com.amazonaws.services.s3.model.ListObjectsRequest; -import com.amazonaws.services.s3.model.ObjectListing; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.services.s3.model.PutObjectResult; -import com.amazonaws.services.s3.model.S3Object; -import com.amazonaws.services.s3.model.S3ObjectInputStream; -import com.amazonaws.services.s3.model.S3ObjectSummary; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.io.Streams; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentMap; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; - -class MockAmazonS3 extends AbstractAmazonS3 { - - private final ConcurrentMap blobs; - private final String bucket; - private final boolean serverSideEncryption; - private final String cannedACL; - private final String storageClass; - - MockAmazonS3(final ConcurrentMap blobs, - final String bucket, - final boolean serverSideEncryption, - final String cannedACL, - final String storageClass) { - this.blobs = Objects.requireNonNull(blobs); - this.bucket = Objects.requireNonNull(bucket); - this.serverSideEncryption = serverSideEncryption; - this.cannedACL = cannedACL; - this.storageClass = storageClass; - } - - @Override - public boolean doesObjectExist(final String bucketName, final String objectName) throws SdkClientException { - assertThat(bucketName, equalTo(bucket)); - return blobs.containsKey(objectName); - } - - @Override - public PutObjectResult putObject(final PutObjectRequest request) throws AmazonClientException { - assertThat(request.getBucketName(), equalTo(bucket)); - assertThat(request.getMetadata().getSSEAlgorithm(), serverSideEncryption ? equalTo("AES256") : nullValue()); - assertThat(request.getCannedAcl(), notNullValue()); - assertThat(request.getCannedAcl().toString(), cannedACL != null ? equalTo(cannedACL) : equalTo("private")); - assertThat(request.getStorageClass(), storageClass != null ? equalTo(storageClass) : equalTo("STANDARD")); - - - final String blobName = request.getKey(); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - try { - Streams.copy(request.getInputStream(), out); - blobs.put(blobName, out.toByteArray()); - } catch (IOException e) { - throw new AmazonClientException(e); - } - return new PutObjectResult(); - } - - @Override - public S3Object getObject(final GetObjectRequest request) throws AmazonClientException { - assertThat(request.getBucketName(), equalTo(bucket)); - - final String blobName = request.getKey(); - final byte[] content = blobs.get(blobName); - if (content == null) { - AmazonS3Exception exception = new AmazonS3Exception("[" + blobName + "] does not exist."); - exception.setStatusCode(404); - throw exception; - } - - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(content.length); - - S3Object s3Object = new S3Object(); - s3Object.setObjectContent(new S3ObjectInputStream(new ByteArrayInputStream(content), null, false)); - s3Object.setKey(blobName); - s3Object.setObjectMetadata(metadata); - - return s3Object; - } - - @Override - public ObjectListing listObjects(final ListObjectsRequest request) throws AmazonClientException { - assertThat(request.getBucketName(), equalTo(bucket)); - - final ObjectListing listing = new ObjectListing(); - listing.setBucketName(request.getBucketName()); - listing.setPrefix(request.getPrefix()); - - for (Map.Entry blob : blobs.entrySet()) { - if (Strings.isEmpty(request.getPrefix()) || blob.getKey().startsWith(request.getPrefix())) { - S3ObjectSummary summary = new S3ObjectSummary(); - summary.setBucketName(request.getBucketName()); - summary.setKey(blob.getKey()); - summary.setSize(blob.getValue().length); - listing.getObjectSummaries().add(summary); - } - } - return listing; - } - - @Override - public void deleteObject(final DeleteObjectRequest request) throws AmazonClientException { - assertThat(request.getBucketName(), equalTo(bucket)); - blobs.remove(request.getKey()); - } - - @Override - public void shutdown() { - // TODO check close - } - - @Override - public DeleteObjectsResult deleteObjects(DeleteObjectsRequest request) throws SdkClientException { - assertThat(request.getBucketName(), equalTo(bucket)); - - final List deletions = new ArrayList<>(); - for (DeleteObjectsRequest.KeyVersion key : request.getKeys()) { - if (blobs.remove(key.getKey()) != null) { - DeleteObjectsResult.DeletedObject deletion = new DeleteObjectsResult.DeletedObject(); - deletion.setKey(key.getKey()); - deletions.add(deletion); - } - } - return new DeleteObjectsResult(deletions); - } -} diff --git a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreContainerTests.java b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreContainerTests.java index 95f51091555c2..25185c0570724 100644 --- a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreContainerTests.java +++ b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreContainerTests.java @@ -34,15 +34,11 @@ import com.amazonaws.services.s3.model.StorageClass; import com.amazonaws.services.s3.model.UploadPartRequest; import com.amazonaws.services.s3.model.UploadPartResult; -import org.elasticsearch.cluster.metadata.RepositoryMetaData; import org.elasticsearch.common.blobstore.BlobPath; -import org.elasticsearch.common.blobstore.BlobStore; import org.elasticsearch.common.blobstore.BlobStoreException; import org.elasticsearch.common.collect.Tuple; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; -import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.repositories.ESBlobStoreContainerTestCase; +import org.elasticsearch.test.ESTestCase; import org.mockito.ArgumentCaptor; import java.io.ByteArrayInputStream; @@ -50,8 +46,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Locale; -import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -65,16 +59,7 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.doAnswer; -public class S3BlobStoreContainerTests extends ESBlobStoreContainerTestCase { - - protected BlobStore newBlobStore() { - return randomMockS3BlobStore(); - } - - @Override - public void testVerifyOverwriteFails() { - assumeFalse("not implemented because of S3's weak consistency model", true); - } +public class S3BlobStoreContainerTests extends ESTestCase { public void testExecuteSingleUploadBlobSizeTooLarge() { final long blobSize = ByteSizeUnit.GB.toBytes(randomIntBetween(6, 10)); @@ -462,35 +447,4 @@ private static void assertNumberOfMultiparts(final int expectedParts, final long assertEquals("Expected number of parts [" + expectedParts + "] but got [" + result.v1() + "]", expectedParts, (long) result.v1()); assertEquals("Expected remaining [" + expectedRemaining + "] but got [" + result.v2() + "]", expectedRemaining, (long) result.v2()); } - - /** - * Creates a new {@link S3BlobStore} with random settings. - *

    - * The blobstore uses a {@link MockAmazonS3} client. - */ - public static S3BlobStore randomMockS3BlobStore() { - String bucket = randomAlphaOfLength(randomIntBetween(1, 10)).toLowerCase(Locale.ROOT); - ByteSizeValue bufferSize = new ByteSizeValue(randomIntBetween(5, 100), ByteSizeUnit.MB); - boolean serverSideEncryption = randomBoolean(); - - String cannedACL = null; - if (randomBoolean()) { - cannedACL = randomFrom(CannedAccessControlList.values()).toString(); - } - - String storageClass = null; - if (randomBoolean()) { - storageClass = randomValueOtherThan(StorageClass.Glacier, () -> randomFrom(StorageClass.values())).toString(); - } - - final AmazonS3 client = new MockAmazonS3(new ConcurrentHashMap<>(), bucket, serverSideEncryption, cannedACL, storageClass); - final S3Service service = new S3Service() { - @Override - public synchronized AmazonS3Reference client(RepositoryMetaData repositoryMetaData) { - return new AmazonS3Reference(client); - } - }; - return new S3BlobStore(service, bucket, serverSideEncryption, bufferSize, cannedACL, storageClass, - new RepositoryMetaData(bucket, "s3", Settings.EMPTY)); - } } diff --git a/server/src/test/java/org/elasticsearch/common/blobstore/fs/FsBlobStoreContainerTests.java b/server/src/test/java/org/elasticsearch/common/blobstore/fs/FsBlobStoreContainerTests.java deleted file mode 100644 index 84e7f58cf4935..0000000000000 --- a/server/src/test/java/org/elasticsearch/common/blobstore/fs/FsBlobStoreContainerTests.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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. - */ -package org.elasticsearch.common.blobstore.fs; - -import org.apache.lucene.util.LuceneTestCase; -import org.elasticsearch.common.blobstore.BlobContainer; -import org.elasticsearch.common.blobstore.BlobPath; -import org.elasticsearch.common.blobstore.BlobStore; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.ByteSizeUnit; -import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.repositories.ESBlobStoreContainerTestCase; -import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -@LuceneTestCase.SuppressFileSystems("ExtrasFS") -public class FsBlobStoreContainerTests extends ESBlobStoreContainerTestCase { - - protected BlobStore newBlobStore() throws IOException { - final Settings settings; - if (randomBoolean()) { - settings = Settings.builder().put("buffer_size", new ByteSizeValue(randomIntBetween(1, 100), ByteSizeUnit.KB)).build(); - } else { - settings = Settings.EMPTY; - } - return new FsBlobStore(settings, createTempDir(), false); - } - - public void testReadOnly() throws Exception { - Path tempDir = createTempDir(); - Path path = tempDir.resolve("bar"); - - try (FsBlobStore store = new FsBlobStore(Settings.EMPTY, path, true)) { - assertFalse(Files.exists(path)); - BlobPath blobPath = BlobPath.cleanPath().add("foo"); - store.blobContainer(blobPath); - Path storePath = store.path(); - for (String d : blobPath) { - storePath = storePath.resolve(d); - } - assertFalse(Files.exists(storePath)); - } - - try (FsBlobStore store = new FsBlobStore(Settings.EMPTY, path, false)) { - assertTrue(Files.exists(path)); - BlobPath blobPath = BlobPath.cleanPath().add("foo"); - BlobContainer container = store.blobContainer(blobPath); - Path storePath = store.path(); - for (String d : blobPath) { - storePath = storePath.resolve(d); - } - assertTrue(Files.exists(storePath)); - assertTrue(Files.isDirectory(storePath)); - - byte[] data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); - writeBlob(container, "test", new BytesArray(data)); - assertArrayEquals(readBlobFully(container, "test", data.length), data); - assertTrue(BlobStoreTestUtil.blobExists(container, "test")); - } - } -} diff --git a/server/src/test/java/org/elasticsearch/repositories/fs/FsBlobStoreRepositoryIT.java b/server/src/test/java/org/elasticsearch/repositories/fs/FsBlobStoreRepositoryIT.java index b79d250eceefd..55eb35500a6cc 100644 --- a/server/src/test/java/org/elasticsearch/repositories/fs/FsBlobStoreRepositoryIT.java +++ b/server/src/test/java/org/elasticsearch/repositories/fs/FsBlobStoreRepositoryIT.java @@ -19,17 +19,21 @@ package org.elasticsearch.repositories.fs; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.blobstore.BlobContainer; +import org.elasticsearch.common.blobstore.BlobPath; +import org.elasticsearch.common.blobstore.fs.FsBlobStore; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; import org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase; import java.io.IOException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.util.concurrent.ExecutionException; import java.util.stream.Stream; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -55,7 +59,7 @@ protected Settings repositorySettings() { return settings.build(); } - public void testMissingDirectoriesNotCreatedInReadonlyRepository() throws IOException, ExecutionException, InterruptedException { + public void testMissingDirectoriesNotCreatedInReadonlyRepository() throws IOException, InterruptedException { final String repoName = randomName(); final Path repoPath = randomRepoPath(); @@ -97,4 +101,37 @@ public void testMissingDirectoriesNotCreatedInReadonlyRepository() throws IOExce assertFalse("deleted path is not recreated in readonly repository", Files.exists(deletedPath)); } + + public void testReadOnly() throws Exception { + Path tempDir = createTempDir(); + Path path = tempDir.resolve("bar"); + + try (FsBlobStore store = new FsBlobStore(Settings.EMPTY, path, true)) { + assertFalse(Files.exists(path)); + BlobPath blobPath = BlobPath.cleanPath().add("foo"); + store.blobContainer(blobPath); + Path storePath = store.path(); + for (String d : blobPath) { + storePath = storePath.resolve(d); + } + assertFalse(Files.exists(storePath)); + } + + try (FsBlobStore store = new FsBlobStore(Settings.EMPTY, path, false)) { + assertTrue(Files.exists(path)); + BlobPath blobPath = BlobPath.cleanPath().add("foo"); + BlobContainer container = store.blobContainer(blobPath); + Path storePath = store.path(); + for (String d : blobPath) { + storePath = storePath.resolve(d); + } + assertTrue(Files.exists(storePath)); + assertTrue(Files.isDirectory(storePath)); + + byte[] data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); + writeBlob(container, "test", new BytesArray(data)); + assertArrayEquals(readBlobFully(container, "test", data.length), data); + assertTrue(BlobStoreTestUtil.blobExists(container, "test")); + } + } } diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java b/test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java deleted file mode 100644 index 3aa65cf392c3d..0000000000000 --- a/test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * 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. - */ -package org.elasticsearch.repositories; - -import org.apache.lucene.util.BytesRef; -import org.apache.lucene.util.BytesRefBuilder; -import org.elasticsearch.common.blobstore.BlobContainer; -import org.elasticsearch.common.blobstore.BlobMetaData; -import org.elasticsearch.common.blobstore.BlobPath; -import org.elasticsearch.common.blobstore.BlobStore; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; -import org.elasticsearch.test.ESTestCase; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.NoSuchFileException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.notNullValue; - -/** - * Generic test case for blob store container implementation. - * These tests check basic blob store functionality. - */ -public abstract class ESBlobStoreContainerTestCase extends ESTestCase { - - public void testReadNonExistingPath() throws IOException { - try(BlobStore store = newBlobStore()) { - final BlobContainer container = store.blobContainer(new BlobPath()); - expectThrows(NoSuchFileException.class, () -> { - try (InputStream is = container.readBlob("non-existing")) { - is.read(); - } - }); - } - } - - public void testWriteRead() throws IOException { - try(BlobStore store = newBlobStore()) { - final BlobContainer container = store.blobContainer(new BlobPath()); - byte[] data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); - writeBlob(container, "foobar", new BytesArray(data), randomBoolean()); - if (randomBoolean()) { - // override file, to check if we get latest contents - data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); - writeBlob(container, "foobar", new BytesArray(data), false); - } - try (InputStream stream = container.readBlob("foobar")) { - BytesRefBuilder target = new BytesRefBuilder(); - while (target.length() < data.length) { - byte[] buffer = new byte[scaledRandomIntBetween(1, data.length - target.length())]; - int offset = scaledRandomIntBetween(0, buffer.length - 1); - int read = stream.read(buffer, offset, buffer.length - offset); - target.append(new BytesRef(buffer, offset, read)); - } - assertEquals(data.length, target.length()); - assertArrayEquals(data, Arrays.copyOfRange(target.bytes(), 0, target.length())); - } - } - } - - public void testList() throws IOException { - try(BlobStore store = newBlobStore()) { - final BlobContainer container = store.blobContainer(new BlobPath()); - assertThat(container.listBlobs().size(), equalTo(0)); - int numberOfFooBlobs = randomIntBetween(0, 10); - int numberOfBarBlobs = randomIntBetween(3, 20); - Map generatedBlobs = new HashMap<>(); - for (int i = 0; i < numberOfFooBlobs; i++) { - int length = randomIntBetween(10, 100); - String name = "foo-" + i + "-"; - generatedBlobs.put(name, (long) length); - writeRandomBlob(container, name, length); - } - for (int i = 1; i < numberOfBarBlobs; i++) { - int length = randomIntBetween(10, 100); - String name = "bar-" + i + "-"; - generatedBlobs.put(name, (long) length); - writeRandomBlob(container, name, length); - } - int length = randomIntBetween(10, 100); - String name = "bar-0-"; - generatedBlobs.put(name, (long) length); - writeRandomBlob(container, name, length); - - Map blobs = container.listBlobs(); - assertThat(blobs.size(), equalTo(numberOfFooBlobs + numberOfBarBlobs)); - for (Map.Entry generated : generatedBlobs.entrySet()) { - BlobMetaData blobMetaData = blobs.get(generated.getKey()); - assertThat(generated.getKey(), blobMetaData, notNullValue()); - assertThat(blobMetaData.name(), equalTo(generated.getKey())); - assertThat(blobMetaData.length(), equalTo(generated.getValue())); - } - - assertThat(container.listBlobsByPrefix("foo-").size(), equalTo(numberOfFooBlobs)); - assertThat(container.listBlobsByPrefix("bar-").size(), equalTo(numberOfBarBlobs)); - assertThat(container.listBlobsByPrefix("baz-").size(), equalTo(0)); - } - } - - public void testDeleteBlobs() throws IOException { - try (BlobStore store = newBlobStore()) { - final List blobNames = Arrays.asList("foobar", "barfoo"); - final BlobContainer container = store.blobContainer(new BlobPath()); - container.deleteBlobsIgnoringIfNotExists(blobNames); // does not raise when blobs don't exist - byte[] data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); - final BytesArray bytesArray = new BytesArray(data); - for (String blobName : blobNames) { - writeBlob(container, blobName, bytesArray, randomBoolean()); - } - assertEquals(container.listBlobs().size(), 2); - container.deleteBlobsIgnoringIfNotExists(blobNames); - assertTrue(container.listBlobs().isEmpty()); - container.deleteBlobsIgnoringIfNotExists(blobNames); // does not raise when blobs don't exist - } - } - - public void testVerifyOverwriteFails() throws IOException { - try (BlobStore store = newBlobStore()) { - final String blobName = "foobar"; - final BlobContainer container = store.blobContainer(new BlobPath()); - byte[] data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); - final BytesArray bytesArray = new BytesArray(data); - writeBlob(container, blobName, bytesArray, true); - // should not be able to overwrite existing blob - expectThrows(FileAlreadyExistsException.class, () -> writeBlob(container, blobName, bytesArray, true)); - container.deleteBlobsIgnoringIfNotExists(Collections.singletonList(blobName)); - writeBlob(container, blobName, bytesArray, true); // after deleting the previous blob, we should be able to write to it again - } - } - - protected void writeBlob(final BlobContainer container, final String blobName, final BytesArray bytesArray, - boolean failIfAlreadyExists) throws IOException { - try (InputStream stream = bytesArray.streamInput()) { - if (randomBoolean()) { - container.writeBlob(blobName, stream, bytesArray.length(), failIfAlreadyExists); - } else { - container.writeBlobAtomic(blobName, stream, bytesArray.length(), failIfAlreadyExists); - } - } - } - - public void testContainerCreationAndDeletion() throws IOException { - try(BlobStore store = newBlobStore()) { - final BlobContainer containerFoo = store.blobContainer(new BlobPath().add("foo")); - final BlobContainer containerBar = store.blobContainer(new BlobPath().add("bar")); - byte[] data1 = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); - byte[] data2 = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); - writeBlob(containerFoo, "test", new BytesArray(data1)); - writeBlob(containerBar, "test", new BytesArray(data2)); - - assertArrayEquals(readBlobFully(containerFoo, "test", data1.length), data1); - assertArrayEquals(readBlobFully(containerBar, "test", data2.length), data2); - - assertTrue(BlobStoreTestUtil.blobExists(containerFoo, "test")); - assertTrue(BlobStoreTestUtil.blobExists(containerBar, "test")); - } - } - - public static byte[] writeRandomBlob(BlobContainer container, String name, int length) throws IOException { - byte[] data = randomBytes(length); - writeBlob(container, name, new BytesArray(data)); - return data; - } - - public static byte[] readBlobFully(BlobContainer container, String name, int length) throws IOException { - byte[] data = new byte[length]; - try (InputStream inputStream = container.readBlob(name)) { - assertThat(inputStream.read(data), equalTo(length)); - assertThat(inputStream.read(), equalTo(-1)); - } - return data; - } - - public static byte[] randomBytes(int length) { - byte[] data = new byte[length]; - for (int i = 0; i < data.length; i++) { - data[i] = (byte) randomInt(); - } - return data; - } - - protected static void writeBlob(BlobContainer container, String blobName, BytesArray bytesArray) throws IOException { - try (InputStream stream = bytesArray.streamInput()) { - container.writeBlob(blobName, stream, bytesArray.length(), true); - } - } - - protected abstract BlobStore newBlobStore() throws IOException; -} diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java index ccf3853ebe64b..f2e96603fcc98 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java @@ -18,7 +18,10 @@ */ package org.elasticsearch.repositories.blobstore; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefBuilder; import org.apache.lucene.util.SetOnce; +import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequestBuilder; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequestBuilder; @@ -27,7 +30,10 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.client.Client; import org.elasticsearch.common.blobstore.BlobContainer; +import org.elasticsearch.common.blobstore.BlobMetaData; +import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.repositories.IndexId; import org.elasticsearch.repositories.RepositoriesService; @@ -37,11 +43,17 @@ import org.elasticsearch.snapshots.SnapshotRestoreException; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.threadpool.ThreadPool; +import org.hamcrest.CoreMatchers; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.NoSuchFileException; import java.util.Arrays; +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 static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -92,6 +104,166 @@ protected final String createRepository(final String name, final Settings settin return name; } + public void testReadNonExistingPath() throws IOException { + try (BlobStore store = newBlobStore()) { + final BlobContainer container = store.blobContainer(new BlobPath()); + expectThrows(NoSuchFileException.class, () -> { + try (InputStream is = container.readBlob("non-existing")) { + is.read(); + } + }); + } + } + + public void testWriteRead() throws IOException { + try (BlobStore store = newBlobStore()) { + final BlobContainer container = store.blobContainer(new BlobPath()); + byte[] data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); + writeBlob(container, "foobar", new BytesArray(data), randomBoolean()); + if (randomBoolean()) { + // override file, to check if we get latest contents + data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); + writeBlob(container, "foobar", new BytesArray(data), false); + } + try (InputStream stream = container.readBlob("foobar")) { + BytesRefBuilder target = new BytesRefBuilder(); + while (target.length() < data.length) { + byte[] buffer = new byte[scaledRandomIntBetween(1, data.length - target.length())]; + int offset = scaledRandomIntBetween(0, buffer.length - 1); + int read = stream.read(buffer, offset, buffer.length - offset); + target.append(new BytesRef(buffer, offset, read)); + } + assertEquals(data.length, target.length()); + assertArrayEquals(data, Arrays.copyOfRange(target.bytes(), 0, target.length())); + } + container.delete(); + } + } + + public void testList() throws IOException { + try (BlobStore store = newBlobStore()) { + final BlobContainer container = store.blobContainer(new BlobPath()); + assertThat(container.listBlobs().size(), CoreMatchers.equalTo(0)); + int numberOfFooBlobs = randomIntBetween(0, 10); + int numberOfBarBlobs = randomIntBetween(3, 20); + Map generatedBlobs = new HashMap<>(); + for (int i = 0; i < numberOfFooBlobs; i++) { + int length = randomIntBetween(10, 100); + String name = "foo-" + i + "-"; + generatedBlobs.put(name, (long) length); + writeRandomBlob(container, name, length); + } + for (int i = 1; i < numberOfBarBlobs; i++) { + int length = randomIntBetween(10, 100); + String name = "bar-" + i + "-"; + generatedBlobs.put(name, (long) length); + writeRandomBlob(container, name, length); + } + int length = randomIntBetween(10, 100); + String name = "bar-0-"; + generatedBlobs.put(name, (long) length); + writeRandomBlob(container, name, length); + + Map blobs = container.listBlobs(); + assertThat(blobs.size(), CoreMatchers.equalTo(numberOfFooBlobs + numberOfBarBlobs)); + for (Map.Entry generated : generatedBlobs.entrySet()) { + BlobMetaData blobMetaData = blobs.get(generated.getKey()); + assertThat(generated.getKey(), blobMetaData, CoreMatchers.notNullValue()); + assertThat(blobMetaData.name(), CoreMatchers.equalTo(generated.getKey())); + assertThat(blobMetaData.length(), CoreMatchers.equalTo(generated.getValue())); + } + + assertThat(container.listBlobsByPrefix("foo-").size(), CoreMatchers.equalTo(numberOfFooBlobs)); + assertThat(container.listBlobsByPrefix("bar-").size(), CoreMatchers.equalTo(numberOfBarBlobs)); + assertThat(container.listBlobsByPrefix("baz-").size(), CoreMatchers.equalTo(0)); + container.delete(); + } + } + + public void testDeleteBlobs() throws IOException { + try (BlobStore store = newBlobStore()) { + final List blobNames = Arrays.asList("foobar", "barfoo"); + final BlobContainer container = store.blobContainer(new BlobPath()); + container.deleteBlobsIgnoringIfNotExists(blobNames); // does not raise when blobs don't exist + byte[] data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); + final BytesArray bytesArray = new BytesArray(data); + for (String blobName : blobNames) { + writeBlob(container, blobName, bytesArray, randomBoolean()); + } + assertEquals(container.listBlobs().size(), 2); + container.deleteBlobsIgnoringIfNotExists(blobNames); + assertTrue(container.listBlobs().isEmpty()); + container.deleteBlobsIgnoringIfNotExists(blobNames); // does not raise when blobs don't exist + } + } + + public static void writeBlob(final BlobContainer container, final String blobName, final BytesArray bytesArray, + boolean failIfAlreadyExists) throws IOException { + try (InputStream stream = bytesArray.streamInput()) { + if (randomBoolean()) { + container.writeBlob(blobName, stream, bytesArray.length(), failIfAlreadyExists); + } else { + container.writeBlobAtomic(blobName, stream, bytesArray.length(), failIfAlreadyExists); + } + } + } + + public void testContainerCreationAndDeletion() throws IOException { + try (BlobStore store = newBlobStore()) { + final BlobContainer containerFoo = store.blobContainer(new BlobPath().add("foo")); + final BlobContainer containerBar = store.blobContainer(new BlobPath().add("bar")); + byte[] data1 = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); + byte[] data2 = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); + writeBlob(containerFoo, "test", new BytesArray(data1)); + writeBlob(containerBar, "test", new BytesArray(data2)); + + assertArrayEquals(readBlobFully(containerFoo, "test", data1.length), data1); + assertArrayEquals(readBlobFully(containerBar, "test", data2.length), data2); + + assertTrue(BlobStoreTestUtil.blobExists(containerFoo, "test")); + assertTrue(BlobStoreTestUtil.blobExists(containerBar, "test")); + containerBar.delete(); + containerFoo.delete(); + } + } + + public static byte[] writeRandomBlob(BlobContainer container, String name, int length) throws IOException { + byte[] data = randomBytes(length); + writeBlob(container, name, new BytesArray(data)); + return data; + } + + public static byte[] readBlobFully(BlobContainer container, String name, int length) throws IOException { + byte[] data = new byte[length]; + try (InputStream inputStream = container.readBlob(name)) { + assertThat(inputStream.read(data), CoreMatchers.equalTo(length)); + assertThat(inputStream.read(), CoreMatchers.equalTo(-1)); + } + return data; + } + + public static byte[] randomBytes(int length) { + byte[] data = new byte[length]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) randomInt(); + } + return data; + } + + protected static void writeBlob(BlobContainer container, String blobName, BytesArray bytesArray) throws IOException { + try (InputStream stream = bytesArray.streamInput()) { + container.writeBlob(blobName, stream, bytesArray.length(), true); + } + } + + protected BlobStore newBlobStore() { + final String repository = createRepository(randomName()); + final BlobStoreRepository blobStoreRepository = + (BlobStoreRepository) internalCluster().getMasterNodeInstance(RepositoriesService.class).repository(repository); + return PlainActionFuture.get( + f -> blobStoreRepository.threadPool().generic().execute(ActionRunnable.supply(f, blobStoreRepository::blobStore))); + } + public void testSnapshotAndRestore() throws Exception { final String repoName = createRepository(randomName()); int indexCount = randomIntBetween(1, 5); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index d5411ebb20b19..5d3db0ad4dbc8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -76,7 +76,7 @@ import java.util.Map; import static java.time.Clock.systemUTC; -import static org.elasticsearch.repositories.ESBlobStoreContainerTestCase.randomBytes; +import static org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase.randomBytes; import static org.elasticsearch.test.ClusterServiceUtils.setState; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; From 20ba8f834f933b44e1a86ff4e940e0a098c3e1c1 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Mon, 16 Dec 2019 09:57:18 +0000 Subject: [PATCH 210/686] Refactor environment variable processing for Docker (#49612) Closes #45223. The current Docker entrypoint script picks up environment variables and translates them into -E command line arguments. However, since any tool executes via `docker exec` doesn't run the entrypoint, it results in a poorer user experience. Therefore, refactor the env var handling so that the -E options are generated in `elasticsearch-env`. These have to be appended to any existing command arguments, since some CLI tools have subcommands and -E arguments must come after the subcommand. Also extract the support for `_FILE` env vars into a separate script, so that it can be called from more than once place (the behaviour is idempotent). Finally, add noop -E handling to CronEvalTool for parity, and support `-E` in MultiCommand before subcommands. --- distribution/docker/src/docker/Dockerfile | 4 +- .../src/docker/bin/docker-entrypoint.sh | 70 ++----------------- distribution/src/bin/elasticsearch-env | 47 +++++++++++++ .../src/bin/elasticsearch-env-from-file | 42 +++++++++++ .../org/elasticsearch/cli/MultiCommand.java | 26 +++++-- .../packaging/test/DockerTests.java | 21 ++++++ .../elasticsearch/packaging/util/Docker.java | 14 +++- .../elasticsearch/cli/MultiCommandTests.java | 57 +++++++++++---- .../configuring-tls-docker.asciidoc | 4 -- .../trigger/schedule/tool/CronEvalTool.java | 4 +- 10 files changed, 193 insertions(+), 96 deletions(-) create mode 100644 distribution/src/bin/elasticsearch-env-from-file diff --git a/distribution/docker/src/docker/Dockerfile b/distribution/docker/src/docker/Dockerfile index 3a42a3e717185..0f9087b143e3d 100644 --- a/distribution/docker/src/docker/Dockerfile +++ b/distribution/docker/src/docker/Dockerfile @@ -29,7 +29,7 @@ ${source_elasticsearch} RUN tar zxf /opt/${elasticsearch} --strip-components=1 RUN grep ES_DISTRIBUTION_TYPE=tar /usr/share/elasticsearch/bin/elasticsearch-env \ - && sed -ie 's/ES_DISTRIBUTION_TYPE=tar/ES_DISTRIBUTION_TYPE=docker/' /usr/share/elasticsearch/bin/elasticsearch-env + && sed -i -e 's/ES_DISTRIBUTION_TYPE=tar/ES_DISTRIBUTION_TYPE=docker/' /usr/share/elasticsearch/bin/elasticsearch-env RUN mkdir -p config data logs RUN chmod 0775 config data logs COPY config/elasticsearch.yml config/log4j2.properties config/ @@ -46,7 +46,7 @@ FROM ${base_image} ENV ELASTIC_CONTAINER true RUN for iter in {1..10}; do ${package_manager} update --setopt=tsflags=nodocs -y && \ - ${package_manager} install --setopt=tsflags=nodocs -y nc shadow-utils && \ + ${package_manager} install --setopt=tsflags=nodocs -y nc shadow-utils zip unzip && \ ${package_manager} clean all && exit_code=0 && break || exit_code=\$? && echo "${package_manager} error: retry \$iter in 10s" && sleep 10; done; \ (exit \$exit_code) diff --git a/distribution/docker/src/docker/bin/docker-entrypoint.sh b/distribution/docker/src/docker/bin/docker-entrypoint.sh index cd418a5415ddc..0366060257b2c 100644 --- a/distribution/docker/src/docker/bin/docker-entrypoint.sh +++ b/distribution/docker/src/docker/bin/docker-entrypoint.sh @@ -42,71 +42,11 @@ fi # contents, and setting an environment variable with the suffix _FILE to # point to it. This can be used to provide secrets to a container, without # the values being specified explicitly when running the container. -for VAR_NAME_FILE in $(env | cut -f1 -d= | grep '_FILE$'); do - if [[ -n "$VAR_NAME_FILE" ]]; then - VAR_NAME="${VAR_NAME_FILE%_FILE}" - - if env | grep "^${VAR_NAME}="; then - echo "ERROR: Both $VAR_NAME_FILE and $VAR_NAME are set. These are mutually exclusive." >&2 - exit 1 - fi - - if [[ ! -e "${!VAR_NAME_FILE}" ]]; then - echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE does not exist" >&2 - exit 1 - fi - - FILE_PERMS="$(stat -c '%a' ${!VAR_NAME_FILE})" - - if [[ "$FILE_PERMS" != "400" && "$FILE_PERMS" != 600 ]]; then - echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE must have file permissions 400 or 600, but actually has: $FILE_PERMS" >&2 - exit 1 - fi - - echo "Setting $VAR_NAME from $VAR_NAME_FILE at ${!VAR_NAME_FILE}" >&2 - export "$VAR_NAME"="$(cat ${!VAR_NAME_FILE})" - - unset VAR_NAME - # Unset the suffixed environment variable - unset "$VAR_NAME_FILE" - fi -done - -# Parse Docker env vars to customize Elasticsearch -# -# e.g. Setting the env var cluster.name=testcluster # -# will cause Elasticsearch to be invoked with -Ecluster.name=testcluster -# -# see https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html#_setting_default_settings - -declare -a es_opts - -while IFS='=' read -r envvar_key envvar_value -do - # Elasticsearch settings need to have at least two dot separated lowercase - # words, e.g. `cluster.name` - if [[ "$envvar_key" =~ ^[a-z0-9_]+\.[a-z0-9_]+ ]]; then - if [[ ! -z $envvar_value ]]; then - es_opt="-E${envvar_key}=${envvar_value}" - es_opts+=("${es_opt}") - fi - fi -done < <(env) - -# The virtual file /proc/self/cgroup should list the current cgroup -# membership. For each hierarchy, you can follow the cgroup path from -# this file to the cgroup filesystem (usually /sys/fs/cgroup/) and -# introspect the statistics for the cgroup for the given -# hierarchy. Alas, Docker breaks this by mounting the container -# statistics at the root while leaving the cgroup paths as the actual -# paths. Therefore, Elasticsearch provides a mechanism to override -# reading the cgroup path from /proc/self/cgroup and instead uses the -# cgroup path defined the JVM system property -# es.cgroups.hierarchy.override. Therefore, we set this value here so -# that cgroup statistics are available for the container this process -# will run in. -export ES_JAVA_OPTS="-Des.cgroups.hierarchy.override=/ $ES_JAVA_OPTS" +# This is also sourced in elasticsearch-env, and is only needed here +# as well because we use ELASTIC_PASSWORD below. Sourcing this script +# is idempotent. +source /usr/share/elasticsearch/bin/elasticsearch-env-from-file if [[ -f bin/elasticsearch-users ]]; then # Check for the ELASTIC_PASSWORD environment variable to set the @@ -130,4 +70,4 @@ if [[ "$(id -u)" == "0" ]]; then fi fi -run_as_other_user_if_needed /usr/share/elasticsearch/bin/elasticsearch "${es_opts[@]}" +run_as_other_user_if_needed /usr/share/elasticsearch/bin/elasticsearch diff --git a/distribution/src/bin/elasticsearch-env b/distribution/src/bin/elasticsearch-env index 867e19b3edd6b..cbdfbf8facb5c 100644 --- a/distribution/src/bin/elasticsearch-env +++ b/distribution/src/bin/elasticsearch-env @@ -86,4 +86,51 @@ ES_DISTRIBUTION_FLAVOR=${es.distribution.flavor} ES_DISTRIBUTION_TYPE=${es.distribution.type} ES_BUNDLED_JDK=${es.bundled_jdk} +if [[ "$ES_DISTRIBUTION_TYPE" == "docker" ]]; then + # Allow environment variables to be set by creating a file with the + # contents, and setting an environment variable with the suffix _FILE to + # point to it. This can be used to provide secrets to a container, without + # the values being specified explicitly when running the container. + source "$ES_HOME/bin/elasticsearch-env-from-file" + + # Parse Docker env vars to customize Elasticsearch + # + # e.g. Setting the env var cluster.name=testcluster + # + # will cause Elasticsearch to be invoked with -Ecluster.name=testcluster + # + # see https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html#_setting_default_settings + + declare -a es_arg_array + + while IFS='=' read -r envvar_key envvar_value + do + # Elasticsearch settings need to have at least two dot separated lowercase + # words, e.g. `cluster.name` + if [[ "$envvar_key" =~ ^[a-z0-9_]+\.[a-z0-9_]+ ]]; then + if [[ ! -z $envvar_value ]]; then + es_opt="-E${envvar_key}=${envvar_value}" + es_arg_array+=("${es_opt}") + fi + fi + done < <(env) + + # Reset the positional parameters to the es_arg_array values and any existing positional params + set -- "$@" "${es_arg_array[@]}" + + # The virtual file /proc/self/cgroup should list the current cgroup + # membership. For each hierarchy, you can follow the cgroup path from + # this file to the cgroup filesystem (usually /sys/fs/cgroup/) and + # introspect the statistics for the cgroup for the given + # hierarchy. Alas, Docker breaks this by mounting the container + # statistics at the root while leaving the cgroup paths as the actual + # paths. Therefore, Elasticsearch provides a mechanism to override + # reading the cgroup path from /proc/self/cgroup and instead uses the + # cgroup path defined the JVM system property + # es.cgroups.hierarchy.override. Therefore, we set this value here so + # that cgroup statistics are available for the container this process + # will run in. + export ES_JAVA_OPTS="-Des.cgroups.hierarchy.override=/ $ES_JAVA_OPTS" +fi + cd "$ES_HOME" diff --git a/distribution/src/bin/elasticsearch-env-from-file b/distribution/src/bin/elasticsearch-env-from-file new file mode 100644 index 0000000000000..fd5326afcc6b7 --- /dev/null +++ b/distribution/src/bin/elasticsearch-env-from-file @@ -0,0 +1,42 @@ +#!/bin/bash + +set -e -o pipefail + +# Allow environment variables to be set by creating a file with the +# contents, and setting an environment variable with the suffix _FILE to +# point to it. This can be used to provide secrets to a container, without +# the values being specified explicitly when running the container. +# +# This script is intended to be sourced, not executed, and modifies the +# environment. + +for VAR_NAME_FILE in $(env | cut -f1 -d= | grep '_FILE$'); do + if [[ -n "$VAR_NAME_FILE" ]]; then + VAR_NAME="${VAR_NAME_FILE%_FILE}" + + if env | grep "^${VAR_NAME}="; then + echo "ERROR: Both $VAR_NAME_FILE and $VAR_NAME are set. These are mutually exclusive." >&2 + exit 1 + fi + + if [[ ! -e "${!VAR_NAME_FILE}" ]]; then + echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE does not exist" >&2 + exit 1 + fi + + FILE_PERMS="$(stat -c '%a' ${!VAR_NAME_FILE})" + + if [[ "$FILE_PERMS" != "400" && "$FILE_PERMS" != 600 ]]; then + echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE must have file permissions 400 or 600, but actually has: $FILE_PERMS" >&2 + exit 1 + fi + + echo "Setting $VAR_NAME from $VAR_NAME_FILE at ${!VAR_NAME_FILE}" >&2 + export "$VAR_NAME"="$(cat ${!VAR_NAME_FILE})" + + unset VAR_NAME + # Unset the suffixed environment variable + unset "$VAR_NAME_FILE" + fi +done + diff --git a/libs/cli/src/main/java/org/elasticsearch/cli/MultiCommand.java b/libs/cli/src/main/java/org/elasticsearch/cli/MultiCommand.java index bcc75a2d1be12..e33598638a936 100644 --- a/libs/cli/src/main/java/org/elasticsearch/cli/MultiCommand.java +++ b/libs/cli/src/main/java/org/elasticsearch/cli/MultiCommand.java @@ -21,11 +21,14 @@ import joptsimple.NonOptionArgumentSpec; import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import joptsimple.util.KeyValuePair; import org.elasticsearch.core.internal.io.IOUtils; import java.io.IOException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; /** @@ -36,6 +39,7 @@ public class MultiCommand extends Command { protected final Map subcommands = new LinkedHashMap<>(); private final NonOptionArgumentSpec arguments = parser.nonOptions("command"); + private final OptionSpec settingOption; /** * Construct the multi-command with the specified command description and runnable to execute before main is invoked. @@ -45,6 +49,7 @@ public class MultiCommand extends Command { */ public MultiCommand(final String description, final Runnable beforeMain) { super(description, beforeMain); + this.settingOption = parser.accepts("E", "Configure a setting").withRequiredArg().ofType(KeyValuePair.class); parser.posixlyCorrect(true); } @@ -66,15 +71,24 @@ protected void execute(Terminal terminal, OptionSet options) throws Exception { if (subcommands.isEmpty()) { throw new IllegalStateException("No subcommands configured"); } - String[] args = arguments.values(options).toArray(new String[0]); - if (args.length == 0) { + + // .values(...) returns an unmodifiable list + final List args = new ArrayList<>(arguments.values(options)); + if (args.isEmpty()) { throw new UserException(ExitCodes.USAGE, "Missing command"); } - Command subcommand = subcommands.get(args[0]); + + String subcommandName = args.remove(0); + Command subcommand = subcommands.get(subcommandName); if (subcommand == null) { - throw new UserException(ExitCodes.USAGE, "Unknown command [" + args[0] + "]"); + throw new UserException(ExitCodes.USAGE, "Unknown command [" + subcommandName + "]"); } - subcommand.mainWithoutErrorHandling(Arrays.copyOfRange(args, 1, args.length), terminal); + + for (final KeyValuePair pair : this.settingOption.values(options)) { + args.add("-E" + pair); + } + + subcommand.mainWithoutErrorHandling(args.toArray(new String[0]), terminal); } @Override diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java index c8575902721c0..a9fa84962490f 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java @@ -390,6 +390,27 @@ public void test082EnvironmentVariablesUsingFilesHaveCorrectPermissions() throws ); } + /** + * Check that environment variables are translated to -E options even for commands invoked under + * `docker exec`, where the Docker image's entrypoint is not executed. + */ + public void test83EnvironmentVariablesAreRespectedUnderDockerExec() { + // This test relies on a CLI tool attempting to connect to Elasticsearch, and the + // tool in question is only in the default distribution. + assumeTrue(distribution.isDefault()); + + runContainer(distribution(), null, Map.of("http.host", "this.is.not.valid")); + + // This will fail if the env var above is passed as a -E argument + final Result result = sh.runIgnoreExitCode("elasticsearch-setup-passwords auto"); + + assertFalse("elasticsearch-setup-passwords command should have failed", result.isSuccess()); + assertThat( + result.stdout, + containsString("java.net.UnknownHostException: this.is.not.valid: Name or service not known") + ); + } + /** * Check whether the elasticsearch-certutil tool has been shipped correctly, * and if present then it can execute. diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java index 245806363c6ab..313060cdd3989 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java @@ -46,10 +46,11 @@ import static org.elasticsearch.packaging.util.FileMatcher.p775; import static org.elasticsearch.packaging.util.FileUtils.getCurrentVersion; import static org.elasticsearch.packaging.util.ServerUtils.makeRequest; -import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** @@ -276,7 +277,7 @@ public static class DockerShell extends Shell { protected String[] getScriptCommand(String script) { assert containerId != null; - return super.getScriptCommand("docker exec " + "--user elasticsearch:root " + "--tty " + containerId + " " + script); + return super.getScriptCommand("docker exec --user elasticsearch:root --tty " + containerId + " " + script); } } @@ -438,7 +439,6 @@ private static void verifyOssInstallation(Installation es) { "elasticsearch", "elasticsearch-cli", "elasticsearch-env", - "elasticsearch-enve", "elasticsearch-keystore", "elasticsearch-node", "elasticsearch-plugin", @@ -446,6 +446,14 @@ private static void verifyOssInstallation(Installation es) { ).forEach(executable -> assertPermissionsAndOwnership(es.bin(executable), p755)); Stream.of("LICENSE.txt", "NOTICE.txt", "README.textile").forEach(doc -> assertPermissionsAndOwnership(es.home.resolve(doc), p644)); + + // These are installed to help users who are working with certificates. + Stream.of("zip", "unzip").forEach(cliPackage -> { + // We could run `yum list installed $pkg` but that causes yum to call out to the network. + // rpm does the job just as well. + final Shell.Result result = dockerShell.runIgnoreExitCode("rpm -q " + cliPackage); + assertTrue(cliPackage + " ought to be installed. " + result, result.isSuccess()); + }); } private static void verifyDefaultInstallation(Installation es) { diff --git a/server/src/test/java/org/elasticsearch/cli/MultiCommandTests.java b/server/src/test/java/org/elasticsearch/cli/MultiCommandTests.java index 41fe851ed2561..38c0edaee801e 100644 --- a/server/src/test/java/org/elasticsearch/cli/MultiCommandTests.java +++ b/server/src/test/java/org/elasticsearch/cli/MultiCommandTests.java @@ -19,12 +19,17 @@ package org.elasticsearch.cli; +import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionSet; +import joptsimple.util.KeyValuePair; import org.junit.Before; import java.io.IOException; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import static org.hamcrest.Matchers.containsString; + public class MultiCommandTests extends CommandTestCase { static class DummyMultiCommand extends MultiCommand { @@ -32,8 +37,7 @@ static class DummyMultiCommand extends MultiCommand { final AtomicBoolean closed = new AtomicBoolean(); DummyMultiCommand() { - super("A dummy multi command", () -> { - }); + super("A dummy multi command", () -> {}); } @Override @@ -75,7 +79,23 @@ public void close() throws IOException { } } - DummyMultiCommand multiCommand; + static class DummySettingsSubCommand extends DummySubCommand { + private final ArgumentAcceptingOptionSpec settingOption; + + DummySettingsSubCommand() { + super(); + this.settingOption = parser.accepts("E", "Configure a setting").withRequiredArg().ofType(KeyValuePair.class); + } + + @Override + protected void execute(Terminal terminal, OptionSet options) throws Exception { + final List values = this.settingOption.values(options); + terminal.println("Settings: " + values); + super.execute(terminal, options); + } + } + + private DummyMultiCommand multiCommand; @Before public void setupCommand() { @@ -87,27 +107,21 @@ protected Command newCommand() { return multiCommand; } - public void testNoCommandsConfigured() throws Exception { - IllegalStateException e = expectThrows(IllegalStateException.class, () -> { - execute(); - }); + public void testNoCommandsConfigured() { + IllegalStateException e = expectThrows(IllegalStateException.class, this::execute); assertEquals("No subcommands configured", e.getMessage()); } - public void testUnknownCommand() throws Exception { + public void testUnknownCommand() { multiCommand.subcommands.put("something", new DummySubCommand()); - UserException e = expectThrows(UserException.class, () -> { - execute("somethingelse"); - }); + UserException e = expectThrows(UserException.class, () -> execute("somethingelse")); assertEquals(ExitCodes.USAGE, e.exitCode); assertEquals("Unknown command [somethingelse]", e.getMessage()); } - public void testMissingCommand() throws Exception { + public void testMissingCommand() { multiCommand.subcommands.put("command1", new DummySubCommand()); - UserException e = expectThrows(UserException.class, () -> { - execute(); - }); + UserException e = expectThrows(UserException.class, this::execute); assertEquals(ExitCodes.USAGE, e.exitCode); assertEquals("Missing command", e.getMessage()); } @@ -121,6 +135,19 @@ public void testHelp() throws Exception { assertTrue(output, output.contains("command2")); } + /** + * Check that if -E arguments are passed to the main command, then they are accepted + * and passed on to the subcommand. + */ + public void testSettingsOnMainCommand() throws Exception { + multiCommand.subcommands.put("command1", new DummySettingsSubCommand()); + execute("-Esetting1=value1", "-Esetting2=value2", "command1", "otherArg"); + + String output = terminal.getOutput(); + assertThat(output, containsString("Settings: [setting1=value1, setting2=value2]")); + assertThat(output, containsString("Arguments: [otherArg]")); + } + public void testSubcommandHelp() throws Exception { multiCommand.subcommands.put("command1", new DummySubCommand()); multiCommand.subcommands.put("command2", new DummySubCommand()); diff --git a/x-pack/docs/en/security/securing-communications/configuring-tls-docker.asciidoc b/x-pack/docs/en/security/securing-communications/configuring-tls-docker.asciidoc index ec00220ab1d75..ac4cbcc40053a 100644 --- a/x-pack/docs/en/security/securing-communications/configuring-tls-docker.asciidoc +++ b/x-pack/docs/en/security/securing-communications/configuring-tls-docker.asciidoc @@ -71,7 +71,6 @@ services: image: {docker-image} command: > bash -c ' - yum install -y -q -e 0 unzip; if [[ ! -f /certs/bundle.zip ]]; then bin/elasticsearch-certutil cert --silent --pem --in config/certificates/instances.yml -out /certs/bundle.zip; unzip /certs/bundle.zip -d /certs; <1> @@ -206,9 +205,6 @@ WARNING: Windows users not running PowerShell will need to remove `\` and join l ---- docker exec es01 /bin/bash -c "bin/elasticsearch-setup-passwords \ auto --batch \ --Expack.security.http.ssl.certificate=certificates/es01/es01.crt \ --Expack.security.http.ssl.certificate_authorities=certificates/ca/ca.crt \ --Expack.security.http.ssl.key=certificates/es01/es01.key \ --url https://localhost:9200" ---- -- diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java index 28b2a363ceba3..565bae15ea998 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java @@ -44,6 +44,8 @@ public static void main(String[] args) throws Exception { "The number of future times this expression will be triggered") .withRequiredArg().ofType(Integer.class).defaultsTo(10); this.arguments = parser.nonOptions("expression"); + + parser.accepts("E", "Unused. Only for compatibility with other CLI tools.").withRequiredArg(); } @Override @@ -56,7 +58,7 @@ protected void execute(Terminal terminal, OptionSet options) throws Exception { execute(terminal, args.get(0), count); } - void execute(Terminal terminal, String expression, int count) throws Exception { + private void execute(Terminal terminal, String expression, int count) throws Exception { Cron.validate(expression); terminal.println("Valid!"); From a268fdb123c95a087e18c042421278d04d772ee5 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Mon, 16 Dec 2019 10:14:47 +0000 Subject: [PATCH 211/686] [ML] Fix exception when field is not included and excluded at the same time (#50192) Executing the data frame analytics _explain API with a config that contains a field that is not in the includes list but at the same time is the excludes list results to trying to remove the field twice from the iterator. That causes an `IllegalStateException`. This commit fixes this issue and adds a test that captures the scenario. --- .../extractor/ExtractedFieldsDetector.java | 8 +++---- .../ExtractedFieldsDetectorTests.java | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java index a98d2cb40543b..6897263ee0638 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java @@ -235,14 +235,14 @@ private void applyIncludesExcludes(Set fields, Set includes, Set if (IGNORE_FIELDS.contains(field)) { throw ExceptionsHelper.badRequestException("field [{}] cannot be analyzed", field); } + if (excludes.contains(field)) { + fieldsIterator.remove(); + addExcludedField(field, "field in excludes list", fieldSelection); + } } else { fieldsIterator.remove(); addExcludedField(field, "field not in includes list", fieldSelection); } - if (excludes.contains(field)) { - fieldsIterator.remove(); - addExcludedField(field, "field in excludes list", fieldSelection); - } } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java index 9c55b2a9ac956..6b882d03f2919 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java @@ -233,6 +233,27 @@ public void testDetect_GivenFieldIsBothIncludedAndExcluded() { ); } + public void testDetect_GivenFieldIsNotIncludedAndIsExcluded() { + FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() + .addAggregatableField("foo", "float") + .addAggregatableField("bar", "float") + .build(); + analyzedFields = new FetchSourceContext(true, new String[] {"foo"}, new String[] {"bar"}); + + ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( + SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + Tuple> fieldExtraction = extractedFieldsDetector.detect(); + + List allFields = fieldExtraction.v1().getAllFields(); + assertThat(allFields, hasSize(1)); + assertThat(allFields.stream().map(ExtractedField::getName).collect(Collectors.toList()), contains("foo")); + + assertFieldSelectionContains(fieldExtraction.v2(), + FieldSelection.excluded("bar", Collections.singleton("float"), "field not in includes list"), + FieldSelection.included("foo", Collections.singleton("float"), false, FieldSelection.FeatureType.NUMERICAL) + ); + } + public void testDetect_GivenRegressionAndRequiredFieldHasInvalidType() { FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() .addAggregatableField("some_float", "float") From 0ecad262e75f11e772eeeffefe88436f843946af Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Mon, 16 Dec 2019 12:42:42 +0100 Subject: [PATCH 212/686] Disk threshold decider is enabled by default (#50222) An old comment had survived after the default was flipped. Relates #6204 --- .../routing/allocation/decider/DiskThresholdDecider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java index 60634684ed0c8..5584a69a95e48 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java @@ -19,8 +19,6 @@ package org.elasticsearch.cluster.routing.allocation.decider; -import java.util.Set; - import com.carrotsearch.hppc.cursors.ObjectCursor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -44,6 +42,8 @@ import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; +import java.util.Set; + import static org.elasticsearch.cluster.routing.allocation.DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK_SETTING; import static org.elasticsearch.cluster.routing.allocation.DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK_SETTING; @@ -68,7 +68,7 @@ * exact byte values for free space (like "500mb") * * cluster.routing.allocation.disk.threshold_enabled is used to - * enable or disable this decider. It defaults to false (disabled). + * enable or disable this decider. It defaults to true (enabled). */ public class DiskThresholdDecider extends AllocationDecider { From 0470eef73af8b9e8962c9e8e07cbc43dd86a243d Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 16 Dec 2019 12:47:31 +0100 Subject: [PATCH 213/686] Fix Index Deletion during Snapshot Finalization (#50202) With #45689 making it so that index metadata is written after all shards have been snapshotted we can't delete indices that are part of the upcoming snapshot finalization any longer and it is not sufficient to check if all shards of an index have been snapshotted before deciding that it is safe to delete it. This change forbids deleting any index that is in the process of being snapshot to avoid issues during snapshot finalization. Relates #50200 (doesn't fully fix yet because we're not fixing the `partial=true` snapshot case here --- .../snapshots/SnapshotsService.java | 19 ++------ .../snapshots/SnapshotResiliencyTests.java | 45 +++++++++++++++++++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index a48f893dfd408..7c638bc332edc 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -1521,21 +1521,10 @@ public static Set snapshottingIndices(final ClusterState currentState, fi final Set indices = new HashSet<>(); for (final SnapshotsInProgress.Entry entry : snapshots.entries()) { if (entry.partial() == false) { - if (entry.state() == State.INIT) { - for (IndexId index : entry.indices()) { - IndexMetaData indexMetaData = currentState.metaData().index(index.getName()); - if (indexMetaData != null && indicesToCheck.contains(indexMetaData.getIndex())) { - indices.add(indexMetaData.getIndex()); - } - } - } else { - for (ObjectObjectCursor shard : entry.shards()) { - Index index = shard.key.getIndex(); - if (indicesToCheck.contains(index) - && shard.value.state().completed() == false - && currentState.getMetaData().index(index) != null) { - indices.add(index); - } + for (IndexId index : entry.indices()) { + IndexMetaData indexMetaData = currentState.metaData().index(index.getName()); + if (indexMetaData != null && indicesToCheck.contains(indexMetaData.getIndex())) { + indices.add(indexMetaData.getIndex()); } } } diff --git a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java index 28d2beb2efcc4..a0e7e51098681 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java @@ -80,6 +80,7 @@ import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.AutoCreateIndex; import org.elasticsearch.action.support.DestructiveOperations; +import org.elasticsearch.action.support.GroupedActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.TransportAction; import org.elasticsearch.action.support.WriteRequest; @@ -502,6 +503,50 @@ public void testConcurrentSnapshotCreateAndDeleteOther() { } } + public void testConcurrentSnapshotDeleteAndDeleteIndex() { + setupTestCluster(randomFrom(1, 3, 5), randomIntBetween(2, 10)); + + String repoName = "repo"; + String snapshotName = "snapshot"; + final String index = "test"; + + TestClusterNodes.TestClusterNode masterNode = + testClusterNodes.currentMaster(testClusterNodes.nodes.values().iterator().next().clusterService.state()); + + final StepListener> createIndicesListener = new StepListener<>(); + + continueOrDie(createRepoAndIndex(repoName, index, 1), createIndexResponse -> { + // create a few more indices to make it more likely that the subsequent index delete operation happens before snapshot + // finalization + final int indices = randomIntBetween(5, 20); + final GroupedActionListener listener = new GroupedActionListener<>(createIndicesListener, indices); + for (int i = 0; i < indices; ++i) { + client().admin().indices().create(new CreateIndexRequest("index-" + i), listener); + } + }); + + final StepListener createSnapshotResponseStepListener = new StepListener<>(); + + continueOrDie(createIndicesListener, createIndexResponses -> + client().admin().cluster().prepareCreateSnapshot(repoName, snapshotName).setWaitForCompletion(false) + .execute(createSnapshotResponseStepListener)); + + continueOrDie(createSnapshotResponseStepListener, + createSnapshotResponse -> client().admin().indices().delete(new DeleteIndexRequest(index), noopListener())); + + deterministicTaskQueue.runAllRunnableTasks(); + + SnapshotsInProgress finalSnapshotsInProgress = masterNode.clusterService.state().custom(SnapshotsInProgress.TYPE); + assertFalse(finalSnapshotsInProgress.entries().stream().anyMatch(entry -> entry.state().completed() == false)); + final Repository repository = masterNode.repositoriesService.repository(repoName); + Collection snapshotIds = getRepositoryData(repository).getSnapshotIds(); + assertThat(snapshotIds, hasSize(1)); + + final SnapshotInfo snapshotInfo = repository.getSnapshotInfo(snapshotIds.iterator().next()); + assertEquals(SnapshotState.SUCCESS, snapshotInfo.state()); + assertEquals(0, snapshotInfo.failedShards()); + } + /** * Simulates concurrent restarts of data and master nodes as well as relocating a primary shard, while starting and subsequently * deleting a snapshot. From d274a6ac364565a272b23f140ee183b50a2daef4 Mon Sep 17 00:00:00 2001 From: Andrew Odendaal Date: Mon, 16 Dec 2019 13:30:05 +0000 Subject: [PATCH 214/686] =?UTF-8?q?[Docs]=C2=A0Fix=20typo=20in=20README=20?= =?UTF-8?q?(#50229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.textile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.textile b/README.textile index 985665811adea..2f7b0472c238e 100644 --- a/README.textile +++ b/README.textile @@ -191,7 +191,7 @@ h3. Distributed, Highly Available Let's face it, things will fail.... -Elasticsearch is a highly available and distributed search engine. Each index is broken down into shards, and each shard can have one or more replicas. By default, an index is created with 1 shards and 1 replica per shard (1/1). There are many topologies that can be used, including 1/10 (improve search performance), or 20/1 (improve indexing performance, with search executed in a map reduce fashion across shards). +Elasticsearch is a highly available and distributed search engine. Each index is broken down into shards, and each shard can have one or more replicas. By default, an index is created with 1 shard and 1 replica per shard (1/1). There are many topologies that can be used, including 1/10 (improve search performance), or 20/1 (improve indexing performance, with search executed in a map reduce fashion across shards). In order to play with the distributed nature of Elasticsearch, simply bring more nodes up and shut down nodes. The system will continue to serve requests (make sure you use the correct http port) with the latest data indexed. From 8fc5a50a63fd97cca783d6dbf7c5a9a647567fe8 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Mon, 16 Dec 2019 14:25:22 +0000 Subject: [PATCH 215/686] [ML] Remove usage of base action logger in ml actions (#50074) --- .../xpack/ml/action/TransportCloseJobAction.java | 4 ++++ .../TransportDeleteDataFrameAnalyticsAction.java | 10 +++++----- .../ml/action/TransportDeleteExpiredDataAction.java | 4 ++++ .../ml/action/TransportDeleteForecastAction.java | 4 ++++ .../action/TransportDeleteModelSnapshotAction.java | 4 ++++ .../ml/action/TransportDeleteTrainedModelAction.java | 4 ++-- .../TransportGetDataFrameAnalyticsStatsAction.java | 8 ++++---- .../xpack/ml/action/TransportGetJobsStatsAction.java | 4 ++++ .../ml/action/TransportGetModelSnapshotsAction.java | 4 ++++ .../ml/action/TransportGetOverallBucketsAction.java | 4 ++++ .../xpack/ml/action/TransportKillProcessAction.java | 4 ++++ .../xpack/ml/action/TransportMlInfoAction.java | 4 ++++ .../TransportStartDataFrameAnalyticsAction.java | 12 ++++++------ .../xpack/ml/action/TransportStopDatafeedAction.java | 4 ++++ .../action/TransportUpdateModelSnapshotAction.java | 4 ++++ 15 files changed, 61 insertions(+), 17 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java index bd3f2d205d32a..c79ee507bd9d2 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; @@ -50,6 +52,8 @@ public class TransportCloseJobAction extends TransportTasksAction { + private static final Logger logger = LogManager.getLogger(TransportCloseJobAction.class); + private final ThreadPool threadPool; private final Client client; private final ClusterService clusterService; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java index 96f56fab4e234..9d221f9c68f6f 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java @@ -65,7 +65,7 @@ public class TransportDeleteDataFrameAnalyticsAction extends TransportMasterNodeAction { - private static final Logger LOGGER = LogManager.getLogger(TransportDeleteDataFrameAnalyticsAction.class); + private static final Logger logger = LogManager.getLogger(TransportDeleteDataFrameAnalyticsAction.class); private final Client client; private final MlMemoryTracker memoryTracker; @@ -118,13 +118,13 @@ protected void masterOperation(Task task, DeleteDataFrameAnalyticsAction.Request ActionListener deleteStateHandler = ActionListener.wrap( bulkByScrollResponse -> { if (bulkByScrollResponse.isTimedOut()) { - LOGGER.warn("[{}] DeleteByQuery for state timed out", id); + logger.warn("[{}] DeleteByQuery for state timed out", id); } if (bulkByScrollResponse.getBulkFailures().isEmpty() == false) { - LOGGER.warn("[{}] {} failures and {} conflicts encountered while runnint DeleteByQuery for state", id, + logger.warn("[{}] {} failures and {} conflicts encountered while runnint DeleteByQuery for state", id, bulkByScrollResponse.getBulkFailures().size(), bulkByScrollResponse.getVersionConflicts()); for (BulkItemResponse.Failure failure : bulkByScrollResponse.getBulkFailures()) { - LOGGER.warn("[{}] DBQ failure: {}", id, failure); + logger.warn("[{}] DBQ failure: {}", id, failure); } } deleteConfig(parentTaskClient, id, listener); @@ -153,7 +153,7 @@ private void deleteConfig(ParentTaskAssigningClient parentTaskClient, String id, return; } assert deleteResponse.getResult() == DocWriteResponse.Result.DELETED; - LOGGER.info("[{}] Deleted", id); + logger.info("[{}] Deleted", id); auditor.info(id, Messages.DATA_FRAME_ANALYTICS_AUDIT_DELETED); listener.onResponse(new AcknowledgedResponse(true)); }, diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteExpiredDataAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteExpiredDataAction.java index c799d57d74f6b..bc4bb6e16e53c 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteExpiredDataAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteExpiredDataAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; @@ -37,6 +39,8 @@ public class TransportDeleteExpiredDataAction extends HandledTransportAction { + private static final Logger logger = LogManager.getLogger(TransportDeleteExpiredDataAction.class); + // TODO: make configurable in the request static final Duration MAX_DURATION = Duration.ofHours(8); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java index 1e5659bd23398..3b5f0b16e2d44 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.ResourceNotFoundException; @@ -66,6 +68,8 @@ public class TransportDeleteForecastAction extends HandledTransportAction { + private static final Logger logger = LogManager.getLogger(TransportDeleteForecastAction.class); + private final Client client; private static final int MAX_FORECAST_TO_SEARCH = 10_000; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteModelSnapshotAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteModelSnapshotAction.java index 8071adc34a021..97a6c9c8a784a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteModelSnapshotAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteModelSnapshotAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; @@ -29,6 +31,8 @@ public class TransportDeleteModelSnapshotAction extends HandledTransportAction { + private static final Logger logger = LogManager.getLogger(TransportDeleteModelSnapshotAction.class); + private final Client client; private final JobManager jobManager; private final JobResultsProvider jobResultsProvider; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAction.java index aadcb9dd34708..55d4eba05ecc4 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAction.java @@ -47,7 +47,7 @@ public class TransportDeleteTrainedModelAction extends TransportMasterNodeAction { - private static final Logger LOGGER = LogManager.getLogger(TransportDeleteTrainedModelAction.class); + private static final Logger logger = LogManager.getLogger(TransportDeleteTrainedModelAction.class); private final TrainedModelProvider trainedModelProvider; private final InferenceAuditor auditor; @@ -120,7 +120,7 @@ private Set getReferencedModelKeys(IngestMetadata ingestMetadata) { .map(InferenceProcessor::getModelId) .forEach(allReferencedModelKeys::add); } catch (Exception ex) { - LOGGER.warn(new ParameterizedMessage("failed to load pipeline [{}]", pipelineId), ex); + logger.warn(new ParameterizedMessage("failed to load pipeline [{}]", pipelineId), ex); } } return allReferencedModelKeys; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDataFrameAnalyticsStatsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDataFrameAnalyticsStatsAction.java index 11bdeb33c8bf8..aadd6041ae642 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDataFrameAnalyticsStatsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDataFrameAnalyticsStatsAction.java @@ -65,7 +65,7 @@ public class TransportGetDataFrameAnalyticsStatsAction extends TransportTasksAction> { - private static final Logger LOGGER = LogManager.getLogger(TransportGetDataFrameAnalyticsStatsAction.class); + private static final Logger logger = LogManager.getLogger(TransportGetDataFrameAnalyticsStatsAction.class); private final Client client; @@ -95,7 +95,7 @@ protected GetDataFrameAnalyticsStatsAction.Response newResponse(GetDataFrameAnal @Override protected void taskOperation(GetDataFrameAnalyticsStatsAction.Request request, DataFrameAnalyticsTask task, ActionListener> listener) { - LOGGER.debug("Get stats for running task [{}]", task.getParams().getId()); + logger.debug("Get stats for running task [{}]", task.getParams().getId()); ActionListener> progressListener = ActionListener.wrap( progress -> { @@ -118,7 +118,7 @@ protected void taskOperation(GetDataFrameAnalyticsStatsAction.Request request, D @Override protected void doExecute(Task task, GetDataFrameAnalyticsStatsAction.Request request, ActionListener listener) { - LOGGER.debug("Get stats for data frame analytics [{}]", request.getId()); + logger.debug("Get stats for data frame analytics [{}]", request.getId()); ActionListener getResponseListener = ActionListener.wrap( getResponse -> { @@ -221,7 +221,7 @@ private StoredProgress parseStoredProgress(SearchHit hit) { StoredProgress storedProgress = StoredProgress.PARSER.apply(parser, null); return storedProgress; } catch (IOException e) { - LOGGER.error(new ParameterizedMessage("failed to parse progress from doc with it [{}]", hit.getId()), e); + logger.error(new ParameterizedMessage("failed to parse progress from doc with it [{}]", hit.getId()), e); return new StoredProgress(Collections.emptyList()); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetJobsStatsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetJobsStatsAction.java index 2ad70e996b51f..d11abcba7e51d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetJobsStatsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetJobsStatsAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.FailedNodeException; import org.elasticsearch.action.TaskOperationFailure; @@ -50,6 +52,8 @@ public class TransportGetJobsStatsAction extends TransportTasksAction> { + private static final Logger logger = LogManager.getLogger(TransportGetJobsStatsAction.class); + private final ClusterService clusterService; private final AutodetectProcessManager processManager; private final JobResultsProvider jobResultsProvider; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetModelSnapshotsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetModelSnapshotsAction.java index 007bdf3c37459..43a42501bd0b7 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetModelSnapshotsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetModelSnapshotsAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; @@ -22,6 +24,8 @@ public class TransportGetModelSnapshotsAction extends HandledTransportAction { + private static final Logger logger = LogManager.getLogger(TransportGetModelSnapshotsAction.class); + private final JobResultsProvider jobResultsProvider; private final JobManager jobManager; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetOverallBucketsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetOverallBucketsAction.java index 8ac9390f8065a..f6c972bfff353 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetOverallBucketsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetOverallBucketsAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; @@ -54,6 +56,8 @@ public class TransportGetOverallBucketsAction extends HandledTransportAction { + private static final Logger logger = LogManager.getLogger(TransportGetOverallBucketsAction.class); + private static final String EARLIEST_TIME = "earliest_time"; private static final String LATEST_TIME = "latest_time"; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java index 33cd31256fe64..90719764e94d6 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -24,6 +26,8 @@ public class TransportKillProcessAction extends TransportJobTaskAction { + private static final Logger logger = LogManager.getLogger(TransportKillProcessAction.class); + private final AnomalyDetectionAuditor auditor; @Inject diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportMlInfoAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportMlInfoAction.java index 0cef48b9ce456..16d83a4b4f9ee 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportMlInfoAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportMlInfoAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; @@ -32,6 +34,8 @@ public class TransportMlInfoAction extends HandledTransportAction { + private static final Logger logger = LogManager.getLogger(TransportMlInfoAction.class); + private final ClusterService clusterService; private final NamedXContentRegistry xContentRegistry; private final Map nativeCodeInfo; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java index 65ed0b93aa7b9..b760f6f02399b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java @@ -93,7 +93,7 @@ public class TransportStartDataFrameAnalyticsAction extends TransportMasterNodeAction { - private static final Logger LOGGER = LogManager.getLogger(TransportStartDataFrameAnalyticsAction.class); + private static final Logger logger = LogManager.getLogger(TransportStartDataFrameAnalyticsAction.class); private final XPackLicenseState licenseState; private final Client client; @@ -254,7 +254,7 @@ private void getStartContext(String id, ActionListener finalListen toValidateMappingsListener.onResponse(startContext); break; case FINISHED: - LOGGER.info("[{}] Job has already finished", startContext.config.getId()); + logger.info("[{}] Job has already finished", startContext.config.getId()); finalListener.onFailure(ExceptionsHelper.badRequestException( "Cannot start because the job has already finished")); break; @@ -478,7 +478,7 @@ public void onResponse(PersistentTasksCustomMetaData.PersistentTask task) { @Override public void onFailure(Exception e) { - LOGGER.error("[" + persistentTask.getParams().getId() + "] Failed to cancel persistent task that could " + + logger.error("[" + persistentTask.getParams().getId() + "] Failed to cancel persistent task that could " + "not be assigned due to [" + exception.getMessage() + "]", e); listener.onFailure(exception); } @@ -554,7 +554,7 @@ public PersistentTasksCustomMetaData.Assignment getAssignment(StartDataFrameAnal if (unavailableIndices.size() != 0) { String reason = "Not opening data frame analytics job [" + id + "], because not all primary shards are active for the following indices [" + String.join(",", unavailableIndices) + "]"; - LOGGER.debug(reason); + logger.debug(reason); return new PersistentTasksCustomMetaData.Assignment(null, reason); } @@ -564,7 +564,7 @@ public PersistentTasksCustomMetaData.Assignment getAssignment(StartDataFrameAnal if (scheduledRefresh) { String reason = "Not opening data frame analytics job [" + id + "] because job memory requirements are stale - refresh requested"; - LOGGER.debug(reason); + logger.debug(reason); return new PersistentTasksCustomMetaData.Assignment(null, reason); } } @@ -580,7 +580,7 @@ public PersistentTasksCustomMetaData.Assignment getAssignment(StartDataFrameAnal @Override protected void nodeOperation(AllocatedPersistentTask task, StartDataFrameAnalyticsAction.TaskParams params, PersistentTaskState state) { - LOGGER.info("[{}] Starting data frame analytics", params.getId()); + logger.info("[{}] Starting data frame analytics", params.getId()); DataFrameAnalyticsTaskState analyticsTaskState = (DataFrameAnalyticsTaskState) state; // If we are "stopping" there is nothing to do diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java index ba197ea0e546d..d90f175e46b1f 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; @@ -45,6 +47,8 @@ public class TransportStopDatafeedAction extends TransportTasksAction { + private static final Logger logger = LogManager.getLogger(TransportStopDatafeedAction.class); + private final ThreadPool threadPool; private final PersistentTasksService persistentTasksService; private final DatafeedConfigProvider datafeedConfigProvider; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpdateModelSnapshotAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpdateModelSnapshotAction.java index 986b5426cef9d..186fba1e003c9 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpdateModelSnapshotAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpdateModelSnapshotAction.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.bulk.BulkAction; @@ -36,6 +38,8 @@ public class TransportUpdateModelSnapshotAction extends HandledTransportAction { + private static final Logger logger = LogManager.getLogger(TransportUpdateModelSnapshotAction.class); + private final JobResultsProvider jobResultsProvider; private final Client client; From 13bfbf32b0e8aa9e5499a201dbbacdbaa9f38e8d Mon Sep 17 00:00:00 2001 From: David Kyle Date: Mon, 16 Dec 2019 14:55:01 +0000 Subject: [PATCH 216/686] [ML] Wait for green after opening job in NetworkDisruptionIT (#50232) Closes #49908 --- .../elasticsearch/xpack/ml/integration/NetworkDisruptionIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/NetworkDisruptionIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/NetworkDisruptionIT.java index 379daba7c45ea..325e34fbf46a7 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/NetworkDisruptionIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/NetworkDisruptionIT.java @@ -48,11 +48,11 @@ public void testJobRelocation() throws Exception { Job.Builder job = createJob("relocation-job", new ByteSizeValue(2, ByteSizeUnit.MB)); PutJobAction.Request putJobRequest = new PutJobAction.Request(job); client().execute(PutJobAction.INSTANCE, putJobRequest).actionGet(); - ensureGreen(); OpenJobAction.Request openJobRequest = new OpenJobAction.Request(job.getId()); AcknowledgedResponse openJobResponse = client().execute(OpenJobAction.INSTANCE, openJobRequest).actionGet(); assertTrue(openJobResponse.isAcknowledged()); + ensureGreen(); // Record which node the job starts off on String origJobNode = awaitJobOpenedAndAssigned(job.getId(), null); From 159dbec488675edb4f60558e5d4631c1c3b1923b Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Mon, 16 Dec 2019 11:08:00 -0500 Subject: [PATCH 217/686] Account trimAboveSeqNo in committed translog generation (#50205) Today we do not consider trimAboveSeqNo when calculating the translog generation of an index commit. If there is no new indexing after the primary promotion, then we won't be able to clean up the translog. --- .../org/elasticsearch/index/translog/Checkpoint.java | 11 +++++++++++ .../org/elasticsearch/index/translog/Translog.java | 8 ++------ .../replication/RecoveryDuringReplicationTests.java | 5 +++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/translog/Checkpoint.java b/server/src/main/java/org/elasticsearch/index/translog/Checkpoint.java index fe20a52f482f3..1e16b9c3a60ae 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/Checkpoint.java +++ b/server/src/main/java/org/elasticsearch/index/translog/Checkpoint.java @@ -106,6 +106,17 @@ private void write(DataOutput out) throws IOException { out.writeLong(trimmedAboveSeqNo); } + /** + * Returns the maximum sequence number of operations in this checkpoint after applying {@link #trimmedAboveSeqNo}. + */ + long maxEffectiveSeqNo() { + if (trimmedAboveSeqNo == SequenceNumbers.UNASSIGNED_SEQ_NO) { + return maxSeqNo; + } else { + return Math.min(trimmedAboveSeqNo, maxSeqNo); + } + } + static Checkpoint emptyTranslogCheckpoint(final long offset, final long generation, final long globalCheckpoint, long minTranslogGeneration) { final long minSeqNo = SequenceNumbers.NO_OPS_PERFORMED; diff --git a/server/src/main/java/org/elasticsearch/index/translog/Translog.java b/server/src/main/java/org/elasticsearch/index/translog/Translog.java index e38880797785b..055b9b82fc917 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/Translog.java +++ b/server/src/main/java/org/elasticsearch/index/translog/Translog.java @@ -696,11 +696,7 @@ private Stream readersAboveMinSeqNo(long minSeqNo) assert readLock.isHeldByCurrentThread() || writeLock.isHeldByCurrentThread() : "callers of readersAboveMinSeqNo must hold a lock: readLock [" + readLock.isHeldByCurrentThread() + "], writeLock [" + readLock.isHeldByCurrentThread() + "]"; - return Stream.concat(readers.stream(), Stream.of(current)) - .filter(reader -> { - final long maxSeqNo = reader.getCheckpoint().maxSeqNo; - return maxSeqNo == SequenceNumbers.UNASSIGNED_SEQ_NO || maxSeqNo >= minSeqNo; - }); + return Stream.concat(readers.stream(), Stream.of(current)).filter(reader -> minSeqNo <= reader.getCheckpoint().maxEffectiveSeqNo()); } /** @@ -1629,7 +1625,7 @@ public TranslogGeneration getMinGenerationForSeqNo(final long seqNo) { */ long minTranslogFileGeneration = this.currentFileGeneration(); for (final TranslogReader reader : readers) { - if (seqNo <= reader.getCheckpoint().maxSeqNo) { + if (seqNo <= reader.getCheckpoint().maxEffectiveSeqNo()) { minTranslogFileGeneration = Math.min(minTranslogFileGeneration, reader.getGeneration()); } } diff --git a/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java b/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java index f6756274642e7..53a96e531585a 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java @@ -800,6 +800,11 @@ public void testRollbackOnPromotion() throws Exception { shards.assertAllEqual(initDocs + inFlightOpsOnNewPrimary + moreDocsAfterRollback); done.set(true); thread.join(); + + for (IndexShard shard : shards) { + shard.flush(new FlushRequest().force(true).waitIfOngoing(true)); + assertThat(shard.translogStats().getUncommittedOperations(), equalTo(0)); + } } } From fef3c272ac44c773b21458df74b53fa9b5a0733a Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Mon, 16 Dec 2019 11:14:45 -0500 Subject: [PATCH 218/686] Migrate MinAggregator integration tests to AggregatorTestCase (#50053) Also renames MinTests to MinAggregationBuilderTests --- ...s.java => MinAggregationBuilderTests.java} | 2 +- .../metrics/MinAggregatorTests.java | 726 +++++++++++++++--- .../search/aggregations/metrics/MinIT.java | 455 ----------- 3 files changed, 606 insertions(+), 577 deletions(-) rename server/src/test/java/org/elasticsearch/search/aggregations/metrics/{MinTests.java => MinAggregationBuilderTests.java} (90%) delete mode 100644 server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinIT.java diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregationBuilderTests.java similarity index 90% rename from server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinTests.java rename to server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregationBuilderTests.java index 549e9916274ae..334921f230296 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregationBuilderTests.java @@ -19,7 +19,7 @@ package org.elasticsearch.search.aggregations.metrics; -public class MinTests extends AbstractNumericMetricTestCase { +public class MinAggregationBuilderTests extends AbstractNumericMetricTestCase { @Override protected MinAggregationBuilder doCreateTestAggregatorFactory() { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java index cfe3c86034f85..922536a76544a 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java @@ -27,6 +27,7 @@ import org.apache.lucene.document.LongPoint; import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.document.SortedNumericDocValuesField; +import org.apache.lucene.document.SortedSetDocValuesField; import org.apache.lucene.document.StringField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexOptions; @@ -34,6 +35,7 @@ import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.MultiReader; import org.apache.lucene.index.NoMergePolicy; import org.apache.lucene.index.RandomIndexWriter; import org.apache.lucene.index.Term; @@ -43,18 +45,46 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.store.Directory; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.CheckedConsumer; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DateFieldMapper; +import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.NumberFieldMapper; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.script.MockScriptEngine; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.script.ScriptModule; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorTestCase; +import org.elasticsearch.search.aggregations.BucketOrder; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.bucket.filter.Filter; +import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.global.InternalGlobal; +import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; +import org.elasticsearch.search.aggregations.bucket.terms.InternalTerms; +import org.elasticsearch.search.aggregations.bucket.terms.LongTerms; +import org.elasticsearch.search.aggregations.bucket.terms.Terms; +import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.support.AggregationInspectionHelper; import org.elasticsearch.search.aggregations.support.FieldContext; +import org.elasticsearch.search.aggregations.support.ValueType; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.search.lookup.LeafDocLookup; import java.io.IOException; import java.util.ArrayList; @@ -62,164 +92,589 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.DoubleConsumer; import java.util.function.Function; import java.util.function.Supplier; +import static java.util.Collections.singleton; +import static org.elasticsearch.index.query.QueryBuilders.termQuery; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class MinAggregatorTests extends AggregatorTestCase { - public void testMinAggregator_numericDv() throws Exception { - Directory directory = newDirectory(); - RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); - Document document = new Document(); - document.add(new NumericDocValuesField("number", 9)); - document.add(new LongPoint("number", 9)); - indexWriter.addDocument(document); - document = new Document(); - document.add(new NumericDocValuesField("number", 7)); - document.add(new LongPoint("number", 7)); - indexWriter.addDocument(document); - document = new Document(); - document.add(new NumericDocValuesField("number", 5)); - document.add(new LongPoint("number", 5)); - indexWriter.addDocument(document); - document = new Document(); - document.add(new NumericDocValuesField("number", 3)); - document.add(new LongPoint("number", 3)); - indexWriter.addDocument(document); - document = new Document(); - document.add(new NumericDocValuesField("number", 1)); - document.add(new LongPoint("number", 1)); - indexWriter.addDocument(document); - document = new Document(); - document.add(new NumericDocValuesField("number", -1)); - document.add(new LongPoint("number", -1)); - indexWriter.addDocument(document); - indexWriter.close(); + private final String SCRIPT_NAME = "script_name"; + private QueryShardContext queryShardContext; + private final long SCRIPT_VALUE = 19L; + + /** Script to take a field name in params and sum the values of the field. */ + private static final String SUM_FIELD_PARAMS_SCRIPT = "sum_field_params"; + + /** Script to sum the values of a field named {@code values}. */ + private static final String SUM_VALUES_FIELD_SCRIPT = "sum_values_field"; + + /** Script to return the value of a field named {@code value}. */ + private static final String VALUE_FIELD_SCRIPT = "value_field"; + + /** Script to return the {@code _value} provided by aggs framework. */ + private static final String VALUE_SCRIPT = "_value"; + + private static final String INVERT_SCRIPT = "invert"; + + @Override + protected ScriptService getMockScriptService() { + Map, Object>> scripts = new HashMap<>(); + Function, Integer> getInc = vars -> { + if (vars == null || vars.containsKey("inc") == false) { + return 0; + } else { + return ((Number) vars.get("inc")).intValue(); + } + }; + + BiFunction, String, Object> sum = (vars, fieldname) -> { + int inc = getInc.apply(vars); + LeafDocLookup docLookup = (LeafDocLookup) vars.get("doc"); + List values = new ArrayList<>(); + for (Object v : docLookup.get(fieldname)) { + values.add(((Number) v).longValue() + inc); + } + return values; + }; + + scripts.put(SCRIPT_NAME, script -> SCRIPT_VALUE); + scripts.put(SUM_FIELD_PARAMS_SCRIPT, vars -> { + String fieldname = (String) vars.get("field"); + return sum.apply(vars, fieldname); + }); + scripts.put(SUM_VALUES_FIELD_SCRIPT, vars -> sum.apply(vars, "values")); + scripts.put(VALUE_FIELD_SCRIPT, vars -> sum.apply(vars, "value")); + scripts.put(VALUE_SCRIPT, vars -> { + int inc = getInc.apply(vars); + return ((Number) vars.get("_value")).doubleValue() + inc; + }); + scripts.put(INVERT_SCRIPT, vars -> -((Number) vars.get("_value")).doubleValue()); + + MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, + scripts, + Collections.emptyMap()); + Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); + + return new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS); + } + + @Override + protected QueryShardContext queryShardContextMock(IndexSearcher searcher, MapperService mapperService, + IndexSettings indexSettings, CircuitBreakerService circuitBreakerService) { + this.queryShardContext = super.queryShardContextMock(searcher, mapperService, indexSettings, circuitBreakerService); + return queryShardContext; + } + + public void testNoMatchingField() throws IOException { + testCase(new MatchAllDocsQuery(), iw -> { + iw.addDocument(singleton(new SortedNumericDocValuesField("wrong_number", 7))); + iw.addDocument(singleton(new SortedNumericDocValuesField("wrong_number", 3))); + }, min -> { + assertEquals(Double.POSITIVE_INFINITY, min.getValue(), 0); + assertFalse(AggregationInspectionHelper.hasValue(min)); + }); + } + + public void testMatchesSortedNumericDocValues() throws IOException { + testCase(new MatchAllDocsQuery(), iw -> { + iw.addDocument(singleton(new SortedNumericDocValuesField("number", 7))); + iw.addDocument(singleton(new SortedNumericDocValuesField("number", 2))); + iw.addDocument(singleton(new SortedNumericDocValuesField("number", 3))); + }, min -> { + assertEquals(2, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }); + } + + public void testMatchesNumericDocValues() throws IOException { + testCase(new MatchAllDocsQuery(), iw -> { + iw.addDocument(singleton(new NumericDocValuesField("number", 7))); + iw.addDocument(singleton(new NumericDocValuesField("number", 2))); + iw.addDocument(singleton(new NumericDocValuesField("number", 3))); + }, min -> { + assertEquals(2, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }); + } + + public void testSomeMatchesSortedNumericDocValues() throws IOException { + testCase(new DocValuesFieldExistsQuery("number"), iw -> { + iw.addDocument(singleton(new SortedNumericDocValuesField("number", 7))); + iw.addDocument(singleton(new SortedNumericDocValuesField("number2", 2))); + iw.addDocument(singleton(new SortedNumericDocValuesField("number", 3))); + }, min -> { + assertEquals(3, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }); + } + + public void testSomeMatchesNumericDocValues() throws IOException { + testCase(new DocValuesFieldExistsQuery("number"), iw -> { + iw.addDocument(singleton(new NumericDocValuesField("number", 7))); + iw.addDocument(singleton(new NumericDocValuesField("number2", 2))); + iw.addDocument(singleton(new NumericDocValuesField("number", 3))); + }, min -> { + assertEquals(3, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }); + } + + public void testQueryFiltering() throws IOException { + testCase(IntPoint.newRangeQuery("number", 0, 3), iw -> { + iw.addDocument(Arrays.asList(new IntPoint("number", 7), new SortedNumericDocValuesField("number", 7))); + iw.addDocument(Arrays.asList(new IntPoint("number", 1), new SortedNumericDocValuesField("number", 1))); + iw.addDocument(Arrays.asList(new IntPoint("number", 3), new SortedNumericDocValuesField("number", 3))); + }, min -> { + assertEquals(1, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }); + } - IndexReader indexReader = DirectoryReader.open(directory); - IndexSearcher indexSearcher = newSearcher(indexReader, true, true); + public void testQueryFiltersAll() throws IOException { + testCase(IntPoint.newRangeQuery("number", -1, 0), iw -> { + iw.addDocument(Arrays.asList(new IntPoint("number", 7), new SortedNumericDocValuesField("number", 7))); + iw.addDocument(Arrays.asList(new IntPoint("number", 1), new SortedNumericDocValuesField("number", 1))); + iw.addDocument(Arrays.asList(new IntPoint("number", 3), new SortedNumericDocValuesField("number", 3))); + }, min -> { + assertEquals(Double.POSITIVE_INFINITY, min.getValue(), 0); + assertFalse(AggregationInspectionHelper.hasValue(min)); + }); + } + + public void testUnmappedWithMissingField() throws IOException { + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min").field("does_not_exist").missing(0L); - MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("_name").field("number"); - MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG); + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); fieldType.setName("number"); - testMinCase(indexSearcher, aggregationBuilder, fieldType, min -> assertEquals(-1.0d, min, 0)); + testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + iw.addDocument(singleton(new NumericDocValuesField("number", 7))); + iw.addDocument(singleton(new NumericDocValuesField("number", 1))); + }, (Consumer) min -> { + assertEquals(0.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }, fieldType); + } - MinAggregator aggregator = createAggregator(aggregationBuilder, indexSearcher, fieldType); - aggregator.preCollection(); - indexSearcher.search(new MatchAllDocsQuery(), aggregator); - aggregator.postCollection(); - InternalMin result = (InternalMin) aggregator.buildAggregation(0L); - assertEquals(-1.0, result.getValue(), 0); - assertTrue(AggregationInspectionHelper.hasValue(result)); + public void testUnsupportedType() { + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min").field("not_a_number"); - indexReader.close(); - directory.close(); + MappedFieldType fieldType = new KeywordFieldMapper.KeywordFieldType(); + fieldType.setName("not_a_number"); + fieldType.setHasDocValues(true); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + iw.addDocument(singleton(new SortedSetDocValuesField("string", new BytesRef("foo")))); + }, (Consumer) min -> { + fail("Should have thrown exception"); + }, fieldType)); + assertEquals(e.getMessage(), "Expected numeric type on field [not_a_number], but got [keyword]"); } - public void testMinAggregator_sortedNumericDv() throws Exception { - Directory directory = newDirectory(); - RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); - Document document = new Document(); - document.add(new SortedNumericDocValuesField("number", 9)); - document.add(new SortedNumericDocValuesField("number", 7)); - document.add(new LongPoint("number", 9)); - document.add(new LongPoint("number", 7)); - indexWriter.addDocument(document); - document = new Document(); - document.add(new SortedNumericDocValuesField("number", 5)); - document.add(new SortedNumericDocValuesField("number", 3)); - document.add(new LongPoint("number", 5)); - document.add(new LongPoint("number", 3)); - indexWriter.addDocument(document); - document = new Document(); - document.add(new SortedNumericDocValuesField("number", 1)); - document.add(new SortedNumericDocValuesField("number", -1)); - document.add(new LongPoint("number", 1)); - document.add(new LongPoint("number", -1)); - indexWriter.addDocument(document); - indexWriter.close(); + public void testBadMissingField() { + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min").field("number").missing("not_a_number"); + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + + expectThrows(NumberFormatException.class, + () -> testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + iw.addDocument(singleton(new NumericDocValuesField("number", 7))); + iw.addDocument(singleton(new NumericDocValuesField("number", 1))); + }, (Consumer) min -> { + fail("Should have thrown exception"); + }, fieldType)); + } - IndexReader indexReader = DirectoryReader.open(directory); - IndexSearcher indexSearcher = newSearcher(indexReader, true, true); + public void testUnmappedWithBadMissingField() { + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min").field("does_not_exist").missing("not_a_number"); - MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("_name").field("number"); - MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG); + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); fieldType.setName("number"); - MinAggregator aggregator = createAggregator(aggregationBuilder, indexSearcher, fieldType); - aggregator.preCollection(); - indexSearcher.search(new MatchAllDocsQuery(), aggregator); - aggregator.postCollection(); - InternalMin result = (InternalMin) aggregator.buildAggregation(0L); - assertEquals(-1.0, result.getValue(), 0); - assertTrue(AggregationInspectionHelper.hasValue(result)); + expectThrows(NumberFormatException.class, + () -> testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + iw.addDocument(singleton(new NumericDocValuesField("number", 7))); + iw.addDocument(singleton(new NumericDocValuesField("number", 1))); + }, (Consumer) min -> { + fail("Should have thrown exception"); + }, fieldType)); + } - indexReader.close(); - directory.close(); + public void testEmptyBucket() throws IOException { + HistogramAggregationBuilder histogram = new HistogramAggregationBuilder("histo").field("number").interval(1).minDocCount(0) + .subAggregation(new MinAggregationBuilder("min").field("number")); + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + + testCase(histogram, new MatchAllDocsQuery(), iw -> { + iw.addDocument(singleton(new NumericDocValuesField("number", 1))); + iw.addDocument(singleton(new NumericDocValuesField("number", 3))); + }, (Consumer) histo -> { + assertThat(histo.getBuckets().size(), equalTo(3)); + + assertNotNull(histo.getBuckets().get(0).getAggregations().asMap().get("min")); + InternalMin min = (InternalMin) histo.getBuckets().get(0).getAggregations().asMap().get("min"); + assertEquals(1.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + + assertNotNull(histo.getBuckets().get(1).getAggregations().asMap().get("min")); + min = (InternalMin) histo.getBuckets().get(1).getAggregations().asMap().get("min"); + assertEquals(Double.POSITIVE_INFINITY, min.getValue(), 0); + assertFalse(AggregationInspectionHelper.hasValue(min)); + + assertNotNull(histo.getBuckets().get(2).getAggregations().asMap().get("min")); + min = (InternalMin) histo.getBuckets().get(2).getAggregations().asMap().get("min"); + assertEquals(3.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + + + }, fieldType); } - public void testMinAggregator_noValue() throws Exception { - Directory directory = newDirectory(); - RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); - Document document = new Document(); - document.add(new SortedNumericDocValuesField("number1", 7)); - indexWriter.addDocument(document); - document = new Document(); - document.add(new SortedNumericDocValuesField("number1", 3)); - indexWriter.addDocument(document); - document = new Document(); - document.add(new SortedNumericDocValuesField("number1", 1)); - indexWriter.addDocument(document); - indexWriter.close(); + public void testFormatter() throws IOException { + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min").field("number").format("0000.0"); + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); - IndexReader indexReader = DirectoryReader.open(directory); - IndexSearcher indexSearcher = newSearcher(indexReader, true, true); + testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + iw.addDocument(singleton(new NumericDocValuesField("number", 7))); + iw.addDocument(singleton(new NumericDocValuesField("number", 1))); + }, (Consumer) min -> { + assertEquals(1.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + assertEquals("0001.0", min.getValueAsString()); + }, fieldType); + } - MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("_name").field("number2"); - MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG); - fieldType.setName("number2"); + public void testGetProperty() throws IOException { + GlobalAggregationBuilder globalBuilder = new GlobalAggregationBuilder("global") + .subAggregation(new MinAggregationBuilder("min").field("number")); - MinAggregator aggregator = createAggregator(aggregationBuilder, indexSearcher, fieldType); - aggregator.preCollection(); - indexSearcher.search(new MatchAllDocsQuery(), aggregator); - aggregator.postCollection(); - InternalMin result = (InternalMin) aggregator.buildAggregation(0L); - assertEquals(Double.POSITIVE_INFINITY, result.getValue(), 0); - assertFalse(AggregationInspectionHelper.hasValue(result)); + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); - indexReader.close(); - directory.close(); + testCase(globalBuilder, new MatchAllDocsQuery(), iw -> { + iw.addDocument(singleton(new NumericDocValuesField("number", 7))); + iw.addDocument(singleton(new NumericDocValuesField("number", 1))); + }, (Consumer) global -> { + assertEquals(1.0, global.getDocCount(), 2); + assertTrue(AggregationInspectionHelper.hasValue(global)); + assertNotNull(global.getAggregations().asMap().get("min")); + + InternalMin min = (InternalMin) global.getAggregations().asMap().get("min"); + assertEquals(1.0, min.getValue(), 0); + assertThat(global.getProperty("min"), equalTo(min)); + assertThat(global.getProperty("min.value"), equalTo(1.0)); + assertThat(min.getProperty("value"), equalTo(1.0)); + }, fieldType); } - public void testMinAggregator_noDocs() throws Exception { - Directory directory = newDirectory(); - RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); - indexWriter.close(); + public void testSingleValuedFieldPartiallyUnmapped() throws IOException { + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min").field("number"); + + try (Directory directory = newDirectory(); + Directory unmappedDirectory = newDirectory()) { + RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 7))); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 2))); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 3))); + indexWriter.close(); + + + RandomIndexWriter unmappedIndexWriter = new RandomIndexWriter(random(), unmappedDirectory); + unmappedIndexWriter.close(); - IndexReader indexReader = DirectoryReader.open(directory); - IndexSearcher indexSearcher = newSearcher(indexReader, true, true); + try (IndexReader indexReader = DirectoryReader.open(directory); + IndexReader unamappedIndexReader = DirectoryReader.open(unmappedDirectory)) { - MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("_name").field("number"); - MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG); + MultiReader multiReader = new MultiReader(indexReader, unamappedIndexReader); + IndexSearcher indexSearcher = newSearcher(multiReader, true, true); + + InternalMin min = searchAndReduce(indexSearcher, new MatchAllDocsQuery(), aggregationBuilder, fieldType); + assertEquals(2.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + } + } + } + + public void testSingleValuedFieldPartiallyUnmappedWithMissing() throws IOException { + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); fieldType.setName("number"); + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min").field("number").missing(-19L); - MinAggregator aggregator = createAggregator(aggregationBuilder, indexSearcher, fieldType); - aggregator.preCollection(); - indexSearcher.search(new MatchAllDocsQuery(), aggregator); - aggregator.postCollection(); - InternalMin result = (InternalMin) aggregator.buildAggregation(0L); - assertEquals(Double.POSITIVE_INFINITY, result.getValue(), 0); - assertFalse(AggregationInspectionHelper.hasValue(result)); + try (Directory directory = newDirectory(); + Directory unmappedDirectory = newDirectory()) { + RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 7))); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 2))); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 3))); + indexWriter.close(); - indexReader.close(); - directory.close(); + + RandomIndexWriter unmappedIndexWriter = new RandomIndexWriter(random(), unmappedDirectory); + unmappedIndexWriter.addDocument(singleton(new NumericDocValuesField("unrelated", 100))); + unmappedIndexWriter.close(); + + try (IndexReader indexReader = DirectoryReader.open(directory); + IndexReader unamappedIndexReader = DirectoryReader.open(unmappedDirectory)) { + + MultiReader multiReader = new MultiReader(indexReader, unamappedIndexReader); + IndexSearcher indexSearcher = newSearcher(multiReader, true, true); + + InternalMin min = searchAndReduce(indexSearcher, new MatchAllDocsQuery(), aggregationBuilder, fieldType); + assertEquals(-19.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + } + } + } + + public void testSingleValuedFieldWithValueScript() throws IOException { + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min") + .field("number") + .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, INVERT_SCRIPT, Collections.emptyMap())); + + testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + final int numDocs = 10; + for (int i = 0; i < numDocs; i++) { + iw.addDocument(singleton(new NumericDocValuesField("number", i + 1))); + } + }, (Consumer) min -> { + assertEquals(-10.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }, fieldType); + } + + public void testSingleValuedFieldWithValueScriptAndMissing() throws IOException { + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min") + .field("number") + .missing(-100L) + .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, INVERT_SCRIPT, Collections.emptyMap())); + + testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + final int numDocs = 10; + for (int i = 0; i < numDocs; i++) { + iw.addDocument(singleton(new NumericDocValuesField("number", i + 1))); + } + iw.addDocument(singleton(new NumericDocValuesField("unrelated", 1))); + }, (Consumer) min -> { + assertEquals(-100.0, min.getValue(), 0); // Note: this comes straight from missing, and is not inverted from script + assertTrue(AggregationInspectionHelper.hasValue(min)); + }, fieldType); + } + + public void testSingleValuedFieldWithValueScriptAndParams() throws IOException { + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min") + .field("number") + .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, VALUE_SCRIPT, Collections.singletonMap("inc", 5))); + + testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + final int numDocs = 10; + for (int i = 0; i < numDocs; i++) { + iw.addDocument(singleton(new NumericDocValuesField("number", i + 1))); + } + }, (Consumer) min -> { + assertEquals(6.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }, fieldType); + } + + public void testScript() throws IOException { + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min") + .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, SCRIPT_NAME, Collections.emptyMap())); + + testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + final int numDocs = 10; + for (int i = 0; i < numDocs; i++) { + iw.addDocument(singleton(new NumericDocValuesField("number", i + 1))); + } + }, (Consumer) min -> { + assertEquals(19.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }, fieldType); + } + + public void testMultiValuedField() throws IOException { + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min").field("number"); + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + + testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + final int numDocs = 10; + for (int i = 0; i < numDocs; i++) { + Document document = new Document(); + document.add(new SortedNumericDocValuesField("number", i + 2)); + document.add(new SortedNumericDocValuesField("number", i + 3)); + iw.addDocument(document); + } + }, (Consumer) min -> { + assertEquals(2.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }, fieldType); + } + + public void testMultiValuedFieldWithScript() throws IOException { + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min") + .field("number") + .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, INVERT_SCRIPT, Collections.emptyMap())); + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + + testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + final int numDocs = 10; + for (int i = 0; i < numDocs; i++) { + Document document = new Document(); + document.add(new SortedNumericDocValuesField("number", i + 2)); + document.add(new SortedNumericDocValuesField("number", i + 3)); + iw.addDocument(document); + } + }, (Consumer) min -> { + assertEquals(-12.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }, fieldType); + } + + public void testMultiValuedFieldWithScriptParams() throws IOException { + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min") + .field("number") + .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, VALUE_SCRIPT, Collections.singletonMap("inc", 5))); + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + + testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> { + final int numDocs = 10; + for (int i = 0; i < numDocs; i++) { + Document document = new Document(); + document.add(new SortedNumericDocValuesField("number", i + 2)); + document.add(new SortedNumericDocValuesField("number", i + 3)); + iw.addDocument(document); + } + }, (Consumer) min -> { + assertEquals(7.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + }, fieldType); + } + + public void testOrderByEmptyAggregation() throws IOException { + AggregationBuilder termsBuilder = new TermsAggregationBuilder("terms", ValueType.NUMERIC) + .field("number") + .order(BucketOrder.compound(BucketOrder.aggregation("filter>min", true))) + .subAggregation(new FilterAggregationBuilder("filter", termQuery("number", 100)) + .subAggregation(new MinAggregationBuilder("min").field("number"))); + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + + int numDocs = 10; + testCase(termsBuilder, new MatchAllDocsQuery(), iw -> { + for (int i = 0; i < numDocs; i++) { + iw.addDocument(singleton(new NumericDocValuesField("number", i + 1))); + } + }, (Consumer>) terms -> { + for (int i = 0; i < numDocs; i++) { + List buckets = terms.getBuckets(); + Terms.Bucket bucket = buckets.get(i); + assertNotNull(bucket); + assertEquals((long) i + 1, bucket.getKeyAsNumber()); + assertEquals(1L, bucket.getDocCount()); + + Filter filter = bucket.getAggregations().get("filter"); + assertNotNull(filter); + assertEquals(0L, filter.getDocCount()); + + InternalMin min = filter.getAggregations().get("min"); + assertNotNull(min); + assertEquals(Double.POSITIVE_INFINITY, min.getValue(), 0); + assertFalse(AggregationInspectionHelper.hasValue(min)); + } + }, fieldType); + } + + public void testCaching() throws IOException { + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min").field("number"); + + try (Directory directory = newDirectory()) { + RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 7))); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 2))); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 3))); + indexWriter.close(); + + + try (IndexReader indexReader = DirectoryReader.open(directory)) { + IndexSearcher indexSearcher = newSearcher(indexReader, true, true); + + InternalMin min = searchAndReduce(indexSearcher, new MatchAllDocsQuery(), aggregationBuilder, fieldType); + assertEquals(2.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + + assertTrue(queryShardContext.isCacheable()); + } + } + } + + public void testNoCachingWithScript() throws IOException { + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min") + .field("number") + .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, INVERT_SCRIPT, Collections.emptyMap()));; + + try (Directory directory = newDirectory()) { + RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 7))); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 2))); + indexWriter.addDocument(singleton(new NumericDocValuesField("number", 3))); + indexWriter.close(); + + + try (IndexReader indexReader = DirectoryReader.open(directory)) { + IndexSearcher indexSearcher = newSearcher(indexReader, true, true); + + InternalMin min = searchAndReduce(indexSearcher, new MatchAllDocsQuery(), aggregationBuilder, fieldType); + assertEquals(-7.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + + assertFalse(queryShardContext.isCacheable()); + } + } } public void testShortcutIsApplicable() { @@ -425,4 +880,33 @@ private ValuesSourceConfig mockDateValuesSourceConfig(Stri when(config.fieldContext()).thenReturn(new FieldContext(fieldName, null, ft)); return config; } + + private void testCase(Query query, + CheckedConsumer buildIndex, + Consumer verify) throws IOException { + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); + fieldType.setName("number"); + MinAggregationBuilder aggregationBuilder = new MinAggregationBuilder("min").field("number"); + testCase(aggregationBuilder, query, buildIndex, verify, fieldType); + } + + private void testCase(T aggregationBuilder, Query query, + CheckedConsumer buildIndex, + Consumer verify, MappedFieldType fieldType) throws IOException { + try (Directory directory = newDirectory()) { + RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); + buildIndex.accept(indexWriter); + indexWriter.close(); + + try (IndexReader indexReader = DirectoryReader.open(directory)) { + IndexSearcher indexSearcher = newSearcher(indexReader, true, true); + + V agg = searchAndReduce(indexSearcher, query, aggregationBuilder, fieldType); + verify.accept(agg); + + } + } + } + + } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinIT.java deleted file mode 100644 index 11df5e1cc96c6..0000000000000 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinIT.java +++ /dev/null @@ -1,455 +0,0 @@ -/* - * 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. - */ -package org.elasticsearch.search.aggregations.metrics; - -import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.script.Script; -import org.elasticsearch.script.ScriptType; -import org.elasticsearch.search.aggregations.AggregationTestScriptsPlugin; -import org.elasticsearch.search.aggregations.Aggregator; -import org.elasticsearch.search.aggregations.InternalAggregation; -import org.elasticsearch.search.aggregations.bucket.filter.Filter; -import org.elasticsearch.search.aggregations.bucket.global.Global; -import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; -import org.elasticsearch.search.aggregations.bucket.terms.Terms; -import org.elasticsearch.search.aggregations.BucketOrder; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static java.util.Collections.emptyMap; -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.elasticsearch.index.query.QueryBuilders.termQuery; -import static org.elasticsearch.search.aggregations.AggregationBuilders.count; -import static org.elasticsearch.search.aggregations.AggregationBuilders.filter; -import static org.elasticsearch.search.aggregations.AggregationBuilders.global; -import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; -import static org.elasticsearch.search.aggregations.AggregationBuilders.min; -import static org.elasticsearch.search.aggregations.AggregationBuilders.terms; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.notNullValue; - -public class MinIT extends AbstractNumericTestCase { - @Override - protected Collection> nodePlugins() { - return Collections.singleton(AggregationTestScriptsPlugin.class); - } - - @Override - public void testEmptyAggregation() throws Exception { - SearchResponse searchResponse = client().prepareSearch("empty_bucket_idx") - .setQuery(matchAllQuery()) - .addAggregation(histogram("histo").field("value").interval(1L).minDocCount(0).subAggregation(min("min").field("value"))) - .get(); - - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); - Histogram histo = searchResponse.getAggregations().get("histo"); - assertThat(histo, notNullValue()); - Histogram.Bucket bucket = histo.getBuckets().get(1); - assertThat(bucket, notNullValue()); - - Min min = bucket.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(Double.POSITIVE_INFINITY)); - } - - @Override - public void testUnmapped() throws Exception { - SearchResponse searchResponse = client().prepareSearch("idx_unmapped") - .setQuery(matchAllQuery()) - .addAggregation(min("min").field("value")) - .get(); - - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(0L)); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(Double.POSITIVE_INFINITY)); - } - - @Override - public void testSingleValuedField() throws Exception { - SearchResponse searchResponse = client().prepareSearch("idx") - .setQuery(matchAllQuery()) - .addAggregation(min("min").field("value")) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(1.0)); - } - - public void testSingleValuedFieldWithFormatter() throws Exception { - SearchResponse searchResponse = client().prepareSearch("idx").setQuery(matchAllQuery()) - .addAggregation(min("min").format("0000.0").field("value")).get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(1.0)); - assertThat(min.getValueAsString(), equalTo("0001.0")); - } - - @Override - public void testSingleValuedFieldGetProperty() throws Exception { - - SearchResponse searchResponse = client().prepareSearch("idx").setQuery(matchAllQuery()) - .addAggregation(global("global").subAggregation(min("min").field("value"))).get(); - - assertHitCount(searchResponse, 10); - - Global global = searchResponse.getAggregations().get("global"); - assertThat(global, notNullValue()); - assertThat(global.getName(), equalTo("global")); - assertThat(global.getDocCount(), equalTo(10L)); - assertThat(global.getAggregations(), notNullValue()); - assertThat(global.getAggregations().asMap().size(), equalTo(1)); - - Min min = global.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - double expectedMinValue = 1.0; - assertThat(min.getValue(), equalTo(expectedMinValue)); - assertThat((Min) ((InternalAggregation)global).getProperty("min"), equalTo(min)); - assertThat((double) ((InternalAggregation)global).getProperty("min.value"), equalTo(expectedMinValue)); - assertThat((double) ((InternalAggregation)min).getProperty("value"), equalTo(expectedMinValue)); - } - - @Override - public void testSingleValuedFieldPartiallyUnmapped() throws Exception { - SearchResponse searchResponse = client().prepareSearch("idx", "idx_unmapped") - .setQuery(matchAllQuery()) - .addAggregation(min("min").field("value")) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(1.0)); - } - - @Override - public void testSingleValuedFieldWithValueScript() throws Exception { - SearchResponse searchResponse = client().prepareSearch("idx") - .setQuery(matchAllQuery()) - .addAggregation( - min("min") - .field("value") - .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(0.0)); - } - - @Override - public void testSingleValuedFieldWithValueScriptWithParams() throws Exception { - Map params = new HashMap<>(); - params.put("dec", 1); - - Script script = new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - dec", params); - - SearchResponse searchResponse = client().prepareSearch("idx") - .setQuery(matchAllQuery()) - .addAggregation(min("min").field("value").script(script)) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(0.0)); - } - - @Override - public void testMultiValuedField() throws Exception { - SearchResponse searchResponse = client().prepareSearch("idx") - .setQuery(matchAllQuery()) - .addAggregation(min("min").field("values")) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(2.0)); - } - - @Override - public void testMultiValuedFieldWithValueScript() throws Exception { - SearchResponse searchResponse = client().prepareSearch("idx") - .setQuery(matchAllQuery()) - .addAggregation( - min("min") - .field("values") - .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(1.0)); - } - - public void testMultiValuedFieldWithValueScriptReverse() throws Exception { - // test what happens when values arrive in reverse order since the min - // aggregator is optimized to work on sorted values - SearchResponse searchResponse = client().prepareSearch("idx").setQuery(matchAllQuery()) - .addAggregation( - min("min") - .field("values") - .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value * -1", emptyMap()))) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(-12d)); - } - - @Override - public void testMultiValuedFieldWithValueScriptWithParams() throws Exception { - Map params = new HashMap<>(); - params.put("dec", 1); - - Script script = new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - dec", params); - - SearchResponse searchResponse = client().prepareSearch("idx").setQuery(matchAllQuery()) - .addAggregation(min("min").field("values").script(script)) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(1.0)); - } - - @Override - public void testScriptSingleValued() throws Exception { - Script script = new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "doc['value'].value", emptyMap()); - - SearchResponse searchResponse = client().prepareSearch("idx").setQuery(matchAllQuery()) - .addAggregation(min("min").script(script)) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(1.0)); - } - - @Override - public void testScriptSingleValuedWithParams() throws Exception { - Map params = new HashMap<>(); - params.put("dec", 1); - - Script script = new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "doc['value'].value - dec", params); - - SearchResponse searchResponse = client().prepareSearch("idx").setQuery(matchAllQuery()) - .addAggregation(min("min").script(script)) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(0.0)); - } - - @Override - public void testScriptMultiValued() throws Exception { - Script script = new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "doc['values']", emptyMap()); - SearchResponse searchResponse = client().prepareSearch("idx").setQuery(matchAllQuery()) - .addAggregation(min("min").script(script)) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(2.0)); - } - - @Override - public void testScriptMultiValuedWithParams() throws Exception { - Map params = new HashMap<>(); - params.put("dec", 1); - - SearchResponse searchResponse = client() - .prepareSearch("idx") - .setQuery(matchAllQuery()) - .addAggregation(min("min").script(AggregationTestScriptsPlugin.DECREMENT_ALL_VALUES)) - .get(); - - assertHitCount(searchResponse, 10); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(1.0)); - } - - @Override - public void testOrderByEmptyAggregation() throws Exception { - SearchResponse searchResponse = client().prepareSearch("idx").setQuery(matchAllQuery()) - .addAggregation(terms("terms").field("value").order(BucketOrder.compound(BucketOrder.aggregation("filter>min", true))) - .subAggregation(filter("filter", termQuery("value", 100)).subAggregation(min("min").field("value")))) - .get(); - - assertHitCount(searchResponse, 10); - - Terms terms = searchResponse.getAggregations().get("terms"); - assertThat(terms, notNullValue()); - List buckets = terms.getBuckets(); - assertThat(buckets, notNullValue()); - assertThat(buckets.size(), equalTo(10)); - - for (int i = 0; i < 10; i++) { - Terms.Bucket bucket = buckets.get(i); - assertThat(bucket, notNullValue()); - assertThat(bucket.getKeyAsNumber(), equalTo((long) i + 1)); - assertThat(bucket.getDocCount(), equalTo(1L)); - Filter filter = bucket.getAggregations().get("filter"); - assertThat(filter, notNullValue()); - assertThat(filter.getDocCount(), equalTo(0L)); - Min min = filter.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.value(), equalTo(Double.POSITIVE_INFINITY)); - - } - } - - /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. - */ - public void testDontCacheScripts() throws Exception { - assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") - .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) - .get()); - indexRandom(true, client().prepareIndex("cache_test_idx").setId("1").setSource("s", 1), - client().prepareIndex("cache_test_idx").setId("2").setSource("s", 2)); - - // Make sure we are starting with a clear cache - assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getHitCount(), equalTo(0L)); - assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getMissCount(), equalTo(0L)); - - // Test that a request using a script does not get cached - SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation( - min("foo").field("d").script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))) - .get(); - assertSearchResponse(r); - - assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getHitCount(), equalTo(0L)); - assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getMissCount(), equalTo(0L)); - - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(min("foo").field("d")).get(); - assertSearchResponse(r); - - assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getHitCount(), equalTo(0L)); - assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getMissCount(), equalTo(1L)); - } - - public void testEarlyTermination() throws Exception { - SearchResponse searchResponse = client().prepareSearch("idx") - .setTrackTotalHits(false) - .setQuery(matchAllQuery()) - .addAggregation(min("min").field("values")) - .addAggregation(count("count").field("values")) - .get(); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(2.0)); - - ValueCount count = searchResponse.getAggregations().get("count"); - assertThat(count.getName(), equalTo("count")); - assertThat(count.getValue(), equalTo(20L)); - } - - public void testNestedEarlyTermination() throws Exception { - SearchResponse searchResponse = client().prepareSearch("idx") - .setTrackTotalHits(false) - .setQuery(matchAllQuery()) - .addAggregation(min("min").field("values")) - .addAggregation(count("count").field("values")) - .addAggregation(terms("terms").field("value") - .collectMode(Aggregator.SubAggCollectionMode.BREADTH_FIRST) - .subAggregation(min("sub_min").field("invalid"))) - .get(); - - Min min = searchResponse.getAggregations().get("min"); - assertThat(min, notNullValue()); - assertThat(min.getName(), equalTo("min")); - assertThat(min.getValue(), equalTo(2.0)); - - ValueCount count = searchResponse.getAggregations().get("count"); - assertThat(count.getName(), equalTo("count")); - assertThat(count.getValue(), equalTo(20L)); - - Terms terms = searchResponse.getAggregations().get("terms"); - assertThat(terms.getBuckets().size(), equalTo(10)); - for (Terms.Bucket b : terms.getBuckets()) { - InternalMin subMin = b.getAggregations().get("sub_min"); - assertThat(subMin.getValue(), equalTo(Double.POSITIVE_INFINITY)); - } - } -} From 49a3e0647483b9f2a8019db5cb7f158d7db2f7a7 Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Mon, 16 Dec 2019 10:37:03 -0800 Subject: [PATCH 219/686] Bump the scroll keep-alive time in cluster upgrade tests. (#50195) In the yaml cluster upgrade tests, we start a scroll in a mixed-version cluster, then attempt to continue the scroll after the upgrade is complete. This test occasionally fails because the scroll can expire before the cluster is done upgrading. The current scroll keep-alive time 5m. This PR bumps it to 10m, which gives a good buffer since in failing tests the time was only exceeded by ~30 seconds. Addresses #46529. --- .../resources/rest-api-spec/test/mixed_cluster/10_basic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/10_basic.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/10_basic.yml index d6daff8c24c31..e8ccd1cf0a2c9 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/10_basic.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/10_basic.yml @@ -32,7 +32,7 @@ rest_total_hits_as_int: true index: upgraded_scroll size: 1 - scroll: 5m + scroll: 10m sort: foo body: query: From 3784e206d90ee884efcc29923be19e990f640cb7 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 16 Dec 2019 10:42:30 -0800 Subject: [PATCH 220/686] [DOCS] Moves model snapshot resource definitions into APIs (#50157) Co-Authored-By: Ed Savage <32410745+edsavage@users.noreply.github.com> --- .../apis/delete-snapshot.asciidoc | 6 +- .../apis/get-snapshot.asciidoc | 151 ++++++++++++++---- .../apis/revert-snapshot.asciidoc | 57 +++---- .../apis/snapshotresource.asciidoc | 104 ------------ .../apis/update-job.asciidoc | 6 +- .../apis/update-snapshot.asciidoc | 13 +- docs/reference/ml/ml-shared.asciidoc | 13 +- docs/reference/redirects.asciidoc | 6 + docs/reference/rest-api/defs.asciidoc | 2 - 9 files changed, 179 insertions(+), 179 deletions(-) delete mode 100644 docs/reference/ml/anomaly-detection/apis/snapshotresource.asciidoc diff --git a/docs/reference/ml/anomaly-detection/apis/delete-snapshot.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-snapshot.asciidoc index b06c6a6a9c1ad..90d0303b9aff3 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-snapshot.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-snapshot.asciidoc @@ -30,10 +30,12 @@ the `model_snapshot_id` in the results from the get jobs API. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the job. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: - (Required, string) Identifier for the model snapshot. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=snapshot-id] [[ml-delete-snapshot-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc index 94b67f6f98b9d..7a7fbfb5a4e75 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-snapshot.asciidoc @@ -30,8 +30,13 @@ Retrieves information about model snapshots. include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: - (Optional, string) Identifier for the model snapshot. If you do not specify - this optional parameter, the API returns information about all model snapshots. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=snapshot-id] ++ +-- +If you do not specify this optional parameter, the API returns information about +all model snapshots. +-- [[ml-get-snapshot-request-body]] ==== {api-request-body-title} @@ -58,52 +63,136 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-get-snapshot-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns an array of model snapshot objects, which have the following +properties: -`model_snapshots`:: - (array) An array of model snapshot objects. For more information, see - <>. +`description`:: +(string) An optional description of the job. + +`job_id`:: +(string) A numerical character string that uniquely identifies the job that + the snapshot was created for. + +`latest_record_time_stamp`:: +(date) The timestamp of the latest processed record. + +`latest_result_time_stamp`:: +(date) The timestamp of the latest bucket result. + +`min_version`:: +(string) The minimum version required to be able to restore the model snapshot. + +`model_size_stats`:: +(object) Summary information describing the model. + +`model_size_stats`.`bucket_allocation_failures_count`::: +(long) The number of buckets for which entities were not processed due to memory +limit constraints. + +`model_size_stats`.`job_id`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +`model_size_stats`.`log_time`::: +(date) The timestamp that the `model_size_stats` were recorded, according to +server-time. + +`model_size_stats`.`memory_status`::: +(string) The status of the memory in relation to its `model_memory_limit`. +Contains one of the following values. ++ +-- +* `hard_limit`: The internal models require more space that the configured +memory limit. Some incoming data could not be processed. +* `ok`: The internal models stayed below the configured value. +* `soft_limit`: The internal models require more than 60% of the configured +memory limit and more aggressive pruning will be performed in order to try to +reclaim space. +-- + +`model_size_stats`.`model_bytes`::: +(long) An approximation of the memory resources required for this analysis. + +`model_size_stats`.`model_bytes_exceeded`::: +(long) The number of bytes over the high limit for memory usage at the last allocation failure. + +`model_size_stats`.`model_bytes_memory_limit`::: +(long) The upper limit for memory usage, checked on increasing values. + +`model_size_stats`.`result_type`::: +(string) Internal. This value is always `model_size_stats`. + +`model_size_stats`.`timestamp`::: +(date) The timestamp that the `model_size_stats` were recorded, according to the +bucket timestamp of the data. + +`model_size_stats`.`total_by_field_count`::: +(long) The number of _by_ field values analyzed. Note that these are counted +separately for each detector and partition. + +`model_size_stats`.`total_over_field_count`::: +(long) The number of _over_ field values analyzed. Note that these are counted +separately for each detector and partition. + +`model_size_stats`.`total_partition_field_count`::: +(long) The number of _partition_ field values analyzed. + +`retain`:: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=retain] + +`snapshot_id`:: +(string) A numerical character string that uniquely identifies the model +snapshot. For example: "1491852978". + +`snapshot_doc_count`:: +(long) For internal use only. + +`timestamp`:: +(date) The creation timestamp for the snapshot. [[ml-get-snapshot-example]] ==== {api-examples-title} [source,console] -------------------------------------------------- -GET _ml/anomaly_detectors/farequote/model_snapshots +GET _ml/anomaly_detectors/high_sum_total_sales/model_snapshots { - "start": "1491852977000" + "start": "1575402236000" } -------------------------------------------------- -// TEST[skip:todo] +// TEST[skip:Kibana sample data] In this example, the API provides a single result: [source,js] ---- { - "count": 1, - "model_snapshots": [ + "count" : 1, + "model_snapshots" : [ { - "job_id": "farequote", - "min_version": "6.3.0", - "timestamp": 1491948163000, - "description": "State persisted due to job close at 2017-04-11T15:02:43-0700", - "snapshot_id": "1491948163", - "snapshot_doc_count": 1, - "model_size_stats": { - "job_id": "farequote", - "result_type": "model_size_stats", - "model_bytes": 387594, - "total_by_field_count": 21, - "total_over_field_count": 0, - "total_partition_field_count": 20, - "bucket_allocation_failures_count": 0, - "memory_status": "ok", - "log_time": 1491948163000, - "timestamp": 1455234600000 + "job_id" : "high_sum_total_sales", + "min_version" : "6.4.0", + "timestamp" : 1575402237000, + "description" : "State persisted due to job close at 2019-12-03T19:43:57+0000", + "snapshot_id" : "1575402237", + "snapshot_doc_count" : 1, + "model_size_stats" : { + "job_id" : "high_sum_total_sales", + "result_type" : "model_size_stats", + "model_bytes" : 1638816, + "model_bytes_exceeded" : 0, + "model_bytes_memory_limit" : 10485760, + "total_by_field_count" : 3, + "total_over_field_count" : 3320, + "total_partition_field_count" : 2, + "bucket_allocation_failures_count" : 0, + "memory_status" : "ok", + "log_time" : 1575402237000, + "timestamp" : 1576965600000 }, - "latest_record_time_stamp": 1455235196000, - "latest_result_time_stamp": 1455234900000, - "retain": false + "latest_record_time_stamp" : 1576971072000, + "latest_result_time_stamp" : 1576965600000, + "retain" : false } ] } diff --git a/docs/reference/ml/anomaly-detection/apis/revert-snapshot.asciidoc b/docs/reference/ml/anomaly-detection/apis/revert-snapshot.asciidoc index d8cfc091b810f..e119cf2b40a33 100644 --- a/docs/reference/ml/anomaly-detection/apis/revert-snapshot.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/revert-snapshot.asciidoc @@ -24,7 +24,7 @@ Reverts to a specific snapshot. [[ml-revert-snapshot-desc]] ==== {api-description-title} -The {ml} feature in {xpack} reacts quickly to anomalous input, learning new +The {ml-features} react quickly to anomalous input, learning new behaviors in data. Highly anomalous input increases the variance in the models whilst the system learns whether this is a new step-change in behavior or a one-off event. In the case where this anomalous input is known to be a one-off, @@ -40,7 +40,8 @@ Friday or a critical system failure. include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: - (Required, string) Identifier for the model snapshot. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=snapshot-id] [[ml-revert-snapshot-request-body]] ==== {api-request-body-title} @@ -57,13 +58,9 @@ If you want to resend data, then delete the intervening results. [[ml-revert-snapshot-example]] ==== {api-examples-title} -The following example reverts to the `1491856080` snapshot for the -`it_ops_new_kpi` job: - [source,console] -------------------------------------------------- -POST -_ml/anomaly_detectors/it_ops_new_kpi/model_snapshots/1491856080/_revert +POST _ml/anomaly_detectors/high_sum_total_sales/model_snapshots/1575402237/_revert { "delete_intervening_results": true } @@ -74,28 +71,32 @@ When the operation is complete, you receive the following results: [source,js] ---- { - "model": { - "job_id": "it_ops_new_kpi", - "min_version": "6.3.0", - "timestamp": 1491856080000, - "description": "State persisted due to job close at 2017-04-10T13:28:00-0700", - "snapshot_id": "1491856080", - "snapshot_doc_count": 1, - "model_size_stats": { - "job_id": "it_ops_new_kpi", - "result_type": "model_size_stats", - "model_bytes": 29518, - "total_by_field_count": 3, - "total_over_field_count": 0, - "total_partition_field_count": 2, - "bucket_allocation_failures_count": 0, - "memory_status": "ok", - "log_time": 1491856080000, - "timestamp": 1455318000000 + "model" : { + "job_id" : "high_sum_total_sales", + "min_version" : "6.4.0", + "timestamp" : 1575402237000, + "description" : "State persisted due to job close at 2019-12-03T19:43:57+0000", + "snapshot_id" : "1575402237", + "snapshot_doc_count" : 1, + "model_size_stats" : { + "job_id" : "high_sum_total_sales", + "result_type" : "model_size_stats", + "model_bytes" : 1638816, + "model_bytes_exceeded" : 0, + "model_bytes_memory_limit" : 10485760, + "total_by_field_count" : 3, + "total_over_field_count" : 3320, + "total_partition_field_count" : 2, + "bucket_allocation_failures_count" : 0, + "memory_status" : "ok", + "log_time" : 1575402237000, + "timestamp" : 1576965600000 }, - "latest_record_time_stamp": 1455318669000, - "latest_result_time_stamp": 1455318000000, - "retain": false + "latest_record_time_stamp" : 1576971072000, + "latest_result_time_stamp" : 1576965600000, + "retain" : false } } ---- + +For a description of these properties, see the <>. diff --git a/docs/reference/ml/anomaly-detection/apis/snapshotresource.asciidoc b/docs/reference/ml/anomaly-detection/apis/snapshotresource.asciidoc deleted file mode 100644 index 13a26e26e8d70..0000000000000 --- a/docs/reference/ml/anomaly-detection/apis/snapshotresource.asciidoc +++ /dev/null @@ -1,104 +0,0 @@ -[role="xpack"] -[testenv="platinum"] -[[ml-snapshot-resource]] -=== Model snapshot resources - -Model snapshots are saved to an internal index within the Elasticsearch cluster. -By default, this is occurs approximately every 3 hours to 4 hours and is -configurable with the `background_persist_interval` property. - -By default, model snapshots are retained for one day (twenty-four hours). You -can change this behavior by updating the `model_snapshot_retention_days` for the -job. When choosing a new value, consider the following: - -* Persistence enables resilience in the event of a system failure. -* Persistence enables snapshots to be reverted. -* The time taken to persist a job is proportional to the size of the model in memory. - -A model snapshot resource has the following properties: - -`description`:: - (string) An optional description of the job. - -`job_id`:: - (string) A numerical character string that uniquely identifies the job that - the snapshot was created for. - -`min_version`:: - (string) The minimum version required to be able to restore the model snapshot. - -`latest_record_time_stamp`:: - (date) The timestamp of the latest processed record. - -`latest_result_time_stamp`:: - (date) The timestamp of the latest bucket result. - -`model_size_stats`:: - (object) Summary information describing the model. - See <>. - -`retain`:: - (boolean) If true, this snapshot will not be deleted during automatic cleanup - of snapshots older than `model_snapshot_retention_days`. - However, this snapshot will be deleted when the job is deleted. - The default value is false. - -`snapshot_id`:: - (string) A numerical character string that uniquely identifies the model - snapshot. For example: "1491852978". - -`snapshot_doc_count`:: - (long) For internal use only. - -`timestamp`:: - (date) The creation timestamp for the snapshot. - -NOTE: All of these properties are informational with the exception of -`description` and `retain`. - -[float] -[[ml-snapshot-stats]] -==== Model Size Statistics - -The `model_size_stats` object has the following properties: - -`bucket_allocation_failures_count`:: - (long) The number of buckets for which entities were not processed due to - memory limit constraints. - -`job_id`:: - (string) A numerical character string that uniquely identifies the job. - -`log_time`:: - (date) The timestamp that the `model_size_stats` were recorded, according to - server-time. - -`memory_status`:: - (string) The status of the memory in relation to its `model_memory_limit`. - Contains one of the following values. - `ok`::: The internal models stayed below the configured value. - `soft_limit`::: The internal models require more than 60% of the configured - memory limit and more aggressive pruning will - be performed in order to try to reclaim space. - `hard_limit`::: The internal models require more space that the configured - memory limit. Some incoming data could not be processed. - -`model_bytes`:: - (long) An approximation of the memory resources required for this analysis. - -`result_type`:: - (string) Internal. This value is always set to "model_size_stats". - -`timestamp`:: - (date) The timestamp that the `model_size_stats` were recorded, according to the bucket timestamp of the data. - -`total_by_field_count`:: - (long) The number of _by_ field values analyzed. Note that these are counted separately for each detector and partition. - -`total_over_field_count`:: - (long) The number of _over_ field values analyzed. Note that these are counted separately for each detector and partition. - -`total_partition_field_count`:: - (long) The number of _partition_ field values analyzed. - -NOTE: All of these properties are informational; you cannot change their values. diff --git a/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc index 09e6e03e57d5f..3a417b6a0fc38 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc @@ -64,9 +64,9 @@ NOTE: You can update the `analysis_limits` only while the job is closed. The `model_memory_limit` property value cannot be decreased below the current usage. TIP: If the `memory_status` property in the -<> has a value of `hard_limit`, -this means that it was unable to process some data. You might want to re-run -the job with an increased `model_memory_limit`. +<> has a value of `hard_limit`, +this means that it was unable to process some data. You might want to re-run the +job with an increased `model_memory_limit`. -- diff --git a/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc index 10f7228fd9b2a..e0220042eccb0 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc @@ -29,7 +29,8 @@ Updates certain properties of a snapshot. include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: - (Required, string) Identifier for the model snapshot. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=snapshot-id] [[ml-update-snapshot-request-body]] ==== {api-request-body-title} @@ -37,14 +38,12 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] The following properties can be updated after the model snapshot is created: `description`:: - (Optional, string) A description of the model snapshot. For example, - "Before black friday". +(Optional, string) A description of the model snapshot. `retain`:: - (Optional, boolean) If true, this snapshot will not be deleted during - automatic cleanup of snapshots older than `model_snapshot_retention_days`. - Note that this snapshot will still be deleted when the {anomaly-job} is - deleted. The default value is false. +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=retain] + [[ml-update-snapshot-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index c13b4e903b91e..5b292f24ef515 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -895,8 +895,7 @@ end::model-plot-config[] tag::model-snapshot-id[] A numerical character string that uniquely identifies the model snapshot. For -example, `1491007364`. For more information about model snapshots, see -<>. +example, `1575402236000 `. end::model-snapshot-id[] tag::model-snapshot-retention-days[] @@ -1006,6 +1005,12 @@ are deleted from {es}. The default value is null, which means results are retained. end::results-retention-days[] +tag::retain[] +If `true`, this snapshot will not be deleted during automatic cleanup of +snapshots older than `model_snapshot_retention_days`. However, this snapshot +will be deleted when the job is deleted. The default value is `false`. +end::retain[] + tag::script-fields[] Specifies scripts that evaluate custom expressions and returns script fields to the {dfeed}. The detector configuration objects in a job can contain functions @@ -1023,6 +1028,10 @@ Specifies the maximum number of {dfanalytics-jobs} to obtain. The default value is `100`. end::size[] +tag::snapshot-id[] +Identifier for the model snapshot. +end::snapshot-id[] + tag::source-put-dfa[] The configuration of how to source the analysis data. It requires an `index`. Optionally, `query` and `_source` may be specified. diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 858e128944742..69d7765e8a9b0 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -1082,3 +1082,9 @@ See [[ml-stats-node]] the details in <>. +[role="exclude",id="ml-snapshot-resource"] +=== Model snapshot resources + +This page was deleted. +[[ml-snapshot-stats]] +See <> and <>. \ No newline at end of file diff --git a/docs/reference/rest-api/defs.asciidoc b/docs/reference/rest-api/defs.asciidoc index 312a889707110..bda7415740d8e 100644 --- a/docs/reference/rest-api/defs.asciidoc +++ b/docs/reference/rest-api/defs.asciidoc @@ -7,14 +7,12 @@ These resource definitions are used in APIs related to {ml-features} and * <> -* <> * <> * <> * <> include::{es-repo-dir}/ml/df-analytics/apis/analysisobjects.asciidoc[] -include::{es-repo-dir}/ml/anomaly-detection/apis/snapshotresource.asciidoc[] include::{xes-repo-dir}/rest-api/security/role-mapping-resources.asciidoc[] include::{es-repo-dir}/ml/anomaly-detection/apis/resultsresource.asciidoc[] include::{es-repo-dir}/transform/apis/transformresource.asciidoc[] From e4d5a4c661b7dc76cc0e6802adc6cf0c35fe80f3 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 16 Dec 2019 15:07:38 -0500 Subject: [PATCH 221/686] Respect ES_PATH_CONF during upgrades too (#50246) A previous commit taught Elasticsearch packages to respect ES_PATH_CONF during installs. Missed in that commit was respecting ES_PATH_CONF on upgrades. This commit does that. Additionally, while ES_PATH_CONF is not currently used in pre-install, this commit adds respect to the preinst script in case we do in the future. --- .../packages/src/common/scripts/posttrans | 15 +++++++++++---- distribution/packages/src/common/scripts/preinst | 7 +++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/distribution/packages/src/common/scripts/posttrans b/distribution/packages/src/common/scripts/posttrans index fdb9aafba38f6..ab989cf5676fd 100644 --- a/distribution/packages/src/common/scripts/posttrans +++ b/distribution/packages/src/common/scripts/posttrans @@ -1,8 +1,15 @@ -if [ ! -f /etc/elasticsearch/elasticsearch.keystore ]; then +# source the default env file +if [ -f "${path.env}" ]; then + . "${path.env}" +else + ES_PATH_CONF="${path.conf}" +fi + +if [ ! -f "${ES_PATH_CONF}"/elasticsearch.keystore ]; then /usr/share/elasticsearch/bin/elasticsearch-keystore create - chown root:elasticsearch /etc/elasticsearch/elasticsearch.keystore - chmod 660 /etc/elasticsearch/elasticsearch.keystore - md5sum /etc/elasticsearch/elasticsearch.keystore > /etc/elasticsearch/.elasticsearch.keystore.initial_md5sum + chown root:elasticsearch "${ES_PATH_CONF}"/elasticsearch.keystore + chmod 660 "${ES_PATH_CONF}"/elasticsearch.keystore + md5sum "${ES_PATH_CONF}"/elasticsearch.keystore > "${ES_PATH_CONF}"/.elasticsearch.keystore.initial_md5sum else /usr/share/elasticsearch/bin/elasticsearch-keystore upgrade fi diff --git a/distribution/packages/src/common/scripts/preinst b/distribution/packages/src/common/scripts/preinst index 66e5038a55daa..c3e15b632aaf7 100644 --- a/distribution/packages/src/common/scripts/preinst +++ b/distribution/packages/src/common/scripts/preinst @@ -15,6 +15,13 @@ err_exit() { exit 1 } +# source the default env file +if [ -f "${path.env}" ]; then + . "${path.env}" +else + ES_PATH_CONF="${path.conf}" +fi + case "$1" in # Debian #################################################### From 75e01afa0bb7a35907a87e96fd3b25039bfbf52f Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Mon, 16 Dec 2019 21:59:54 +0100 Subject: [PATCH 222/686] Recovery buffer size 16B smaller (#50100) G1GC will use humongous allocations when an allocation exceeds half the chosen region size, which is minimum 1MB. By reducing the recovery buffer size by 16 bytes we ensure that the recovery buffer is never allocated as a humongous allocation. --- .../org/elasticsearch/indices/recovery/RecoverySettings.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySettings.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySettings.java index 3db04dec1d69f..469529bb09ff6 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySettings.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySettings.java @@ -81,7 +81,8 @@ public class RecoverySettings { INDICES_RECOVERY_INTERNAL_LONG_ACTION_TIMEOUT_SETTING::get, TimeValue.timeValueSeconds(0), Property.Dynamic, Property.NodeScope); - public static final ByteSizeValue DEFAULT_CHUNK_SIZE = new ByteSizeValue(512, ByteSizeUnit.KB); + // choose 512KB-16B to ensure that the resulting byte[] is not a humongous allocation in G1. + public static final ByteSizeValue DEFAULT_CHUNK_SIZE = new ByteSizeValue(512 * 1024 - 16, ByteSizeUnit.BYTES); private volatile ByteSizeValue maxBytesPerSec; private volatile int maxConcurrentFileChunks; From b7b2c7c60a8f5cea93a8d94bcd912ab4592b1fec Mon Sep 17 00:00:00 2001 From: Przemko Robakowski Date: Tue, 17 Dec 2019 00:18:05 +0100 Subject: [PATCH 223/686] Fix flakiness in CsvProcessorTests (#50254) There's flakiness in CsvProcesorTests, where tests fail if random document generator add field that should not be present. This change cleans generated document from these problematic fields. Closes #50209 --- .../java/org/elasticsearch/ingest/common/CsvProcessorTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorTests.java index 87da73cce129d..45f00a7435033 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorTests.java @@ -209,6 +209,7 @@ private IngestDocument processDocument(String[] headers, String csv) throws Exce private IngestDocument processDocument(String[] headers, String csv, boolean trim) throws Exception { IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random()); + Arrays.stream(headers).filter(ingestDocument::hasField).forEach(ingestDocument::removeField); String fieldName = RandomDocumentPicks.addRandomField(random(), ingestDocument, csv); char quoteChar = quote.isEmpty() ? '"' : quote.charAt(0); From c93e9b187f35adbff2bfe6b9708b46c2c782528b Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Mon, 16 Dec 2019 20:05:14 -0800 Subject: [PATCH 224/686] Modifying WaitForIndexGreen to WaitForIndexColor class --- .../xpack/core/ilm/CloseIndexStep.java | 12 +- .../xpack/core/ilm/ForceMergeAction.java | 49 +++++--- .../xpack/core/ilm/OpenIndexStep.java | 9 +- ...enStep.java => WaitForIndexColorStep.java} | 74 ++++++++++- .../xpack/core/ilm/ForceMergeActionTests.java | 28 +++-- .../ilm/TimeseriesLifecycleTypeTests.java | 5 +- ...s.java => WaitForIndexColorStepTests.java} | 115 +++++++++++++----- 7 files changed, 217 insertions(+), 75 deletions(-) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/{WaitForIndexGreenStep.java => WaitForIndexColorStep.java} (56%) rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/{WaitForIndexGreenStepTests.java => WaitForIndexColorStepTests.java} (51%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java index f0afab5f604ee..994ddf2590b58 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java @@ -12,7 +12,10 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateObserver; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.xpack.core.ilm.AsyncActionStep; + +/** + * Invokes a close step on a single index. + */ public class CloseIndexStep extends AsyncActionStep { public static final String NAME = "close-index"; @@ -24,10 +27,13 @@ public class CloseIndexStep extends AsyncActionStep { @Override public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, ClusterStateObserver observer, Listener listener) { - if(indexMetaData.getState() == IndexMetaData.State.OPEN) { + if (indexMetaData.getState() == IndexMetaData.State.OPEN) { CloseIndexRequest request = new CloseIndexRequest(indexMetaData.getIndex().getName()); getClient().admin().indices() - .close(request, ActionListener.wrap(closeIndexResponse -> listener.onResponse(true), listener::onFailure)); + .close(request, ActionListener.wrap(closeIndexResponse -> { + assert closeIndexResponse.isAcknowledged() : "close index response is not acknowledged"; + listener.onResponse(true); + }, listener::onFailure)); } else { listener.onResponse(true); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index 5d4227a766f58..636f7a83e28c8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -5,7 +5,10 @@ */ package org.elasticsearch.xpack.core.ilm; +import org.apache.lucene.codecs.Codec; +import org.elasticsearch.Version; import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; @@ -30,53 +33,61 @@ public class ForceMergeAction implements LifecycleAction { public static final String NAME = "forcemerge"; public static final ParseField MAX_NUM_SEGMENTS_FIELD = new ParseField("max_num_segments"); - public static final ParseField BEST_COMPRESSION_FIELD = new ParseField("best_compression"); + public static final ParseField CODEC = new ParseField("index.codec"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, a -> { int maxNumSegments = (int) a[0]; - boolean bestCompression = a[1] != null && (boolean) a[1]; - return new ForceMergeAction(maxNumSegments, bestCompression); + Codec codec = a[1] != null ? Codec.forName((String)a[1]): Codec.getDefault(); + return new ForceMergeAction(maxNumSegments, codec); }); static { PARSER.declareInt(ConstructingObjectParser.constructorArg(), MAX_NUM_SEGMENTS_FIELD); - PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), BEST_COMPRESSION_FIELD); + PARSER.declareInt(ConstructingObjectParser.constructorArg(), CODEC); } private final int maxNumSegments; - private final boolean bestCompression; + private final Codec codec; public static ForceMergeAction parse(XContentParser parser) { return PARSER.apply(parser, null); } - public ForceMergeAction(int maxNumSegments, boolean bestCompression) { + public ForceMergeAction(int maxNumSegments, Codec codec) { if (maxNumSegments <= 0) { throw new IllegalArgumentException("[" + MAX_NUM_SEGMENTS_FIELD.getPreferredName() + "] must be a positive integer"); } this.maxNumSegments = maxNumSegments; - this.bestCompression = bestCompression; + this.codec = codec; } public ForceMergeAction(StreamInput in) throws IOException { this.maxNumSegments = in.readVInt(); - this.bestCompression = in.readBoolean(); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + this.codec = Codec.forName(in.readString()); + } else { + this.codec = Codec.getDefault(); + } } public int getMaxNumSegments() { return maxNumSegments; } - public boolean isBestCompression() { - return bestCompression; + public Codec getCodec() { + return this.codec; } @Override public void writeTo(StreamOutput out) throws IOException { out.writeVInt(maxNumSegments); - out.writeBoolean(bestCompression); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeString(codec.getName()); + } else { + out.writeString(Codec.getDefault().getName()); + } } @Override @@ -93,7 +104,7 @@ public boolean isSafeAction() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(MAX_NUM_SEGMENTS_FIELD.getPreferredName(), maxNumSegments); - builder.field(BEST_COMPRESSION_FIELD.getPreferredName(), bestCompression); + builder.field(CODEC.getPreferredName(), codec); builder.endObject(); return builder; } @@ -101,25 +112,25 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override public List toSteps(Client client, String phase, Step.StepKey nextStepKey) { Settings readOnlySettings = Settings.builder().put(IndexMetaData.SETTING_BLOCKS_WRITE, true).build(); + Settings bestCompressionSettings = Settings.builder() + .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), CodecService.BEST_COMPRESSION_CODEC).build(); StepKey readOnlyKey = new StepKey(phase, NAME, ReadOnlyAction.NAME); StepKey forceMergeKey = new StepKey(phase, NAME, ForceMergeStep.NAME); StepKey countKey = new StepKey(phase, NAME, SegmentCountStep.NAME); - if (this.bestCompression) { + if (codec.getName().equals(CodecService.BEST_COMPRESSION_CODEC)) { StepKey closeKey = new StepKey(phase, NAME, CloseIndexStep.NAME); StepKey openKey = new StepKey(phase, NAME, OpenIndexStep.NAME); - StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexGreenStep.NAME); + StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexColorStep.NAME); StepKey updateCompressionKey = new StepKey(phase, NAME, UpdateSettingsStep.NAME); - Settings bestCompressionSettings = Settings.builder() - .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), CodecService.BEST_COMPRESSION_CODEC).build(); CloseIndexStep closeIndexStep = new CloseIndexStep(closeKey, updateCompressionKey, client); UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey, openKey, client, bestCompressionSettings); OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client); - WaitForIndexGreenStep waitForIndexGreenStep = new WaitForIndexGreenStep(waitForGreenIndexKey, forceMergeKey); + WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey, forceMergeKey, ClusterHealthStatus.GREEN); ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, nextStepKey, client, maxNumSegments); return Arrays.asList(closeIndexStep, updateBestCompressionSettings, openIndexStep, waitForIndexGreenStep, forceMergeStep); @@ -133,7 +144,7 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) @Override public int hashCode() { - return Objects.hash(maxNumSegments, bestCompression); + return Objects.hash(maxNumSegments, codec); } @Override @@ -146,7 +157,7 @@ public boolean equals(Object obj) { } ForceMergeAction other = (ForceMergeAction) obj; return Objects.equals(maxNumSegments, other.maxNumSegments) - && Objects.equals(bestCompression, other.bestCompression); + && Objects.equals(codec, other.codec); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java index b032b0761fa44..907750e212332 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java @@ -13,6 +13,10 @@ import org.elasticsearch.cluster.ClusterStateObserver; import org.elasticsearch.cluster.metadata.IndexMetaData; +/** + * Invokes a open step on a single index. + */ + final class OpenIndexStep extends AsyncActionStep { static final String NAME = "open-index"; @@ -28,7 +32,10 @@ public void performAction(IndexMetaData indexMetaData, ClusterState currentClust OpenIndexRequest request = new OpenIndexRequest(indexMetaData.getIndex().getName()); getClient().admin().indices() .open(request, - ActionListener.wrap(closeIndexResponse -> listener.onResponse(true), listener::onFailure)); + ActionListener.wrap(openIndexResponse-> { + assert openIndexResponse.isAcknowledged() : "open Index response is not acknowledged"; + listener.onResponse(true); + }, listener::onFailure)); } else { listener.onResponse(true); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java similarity index 56% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java index d717a970c66b1..812ad66323668 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java @@ -8,6 +8,7 @@ import com.carrotsearch.hppc.cursors.ObjectCursor; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.routing.IndexRoutingTable; import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.RoutingTable; @@ -20,28 +21,91 @@ import java.io.IOException; import java.util.Objects; -class WaitForIndexGreenStep extends ClusterStateWaitStep { +/** + * Wait Step for index based on color + * */ - static final String NAME = "wait-for-index-green-step"; +class WaitForIndexColorStep extends ClusterStateWaitStep { - WaitForIndexGreenStep(StepKey key, StepKey nextStepKey) { + static final String NAME = "wait-for-index-color-step"; + + private final ClusterHealthStatus color; + + WaitForIndexColorStep(StepKey key, StepKey nextStepKey, ClusterHealthStatus color) { super(key, nextStepKey); + this.color = color; + } + + public ClusterHealthStatus getColor() { + return this.color; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), this.color); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (!(obj instanceof WaitForIndexColorStep)) return false; + WaitForIndexColorStep other = (WaitForIndexColorStep) obj; + return super.equals(obj) && Objects.equals(this.color, other.color); } @Override public Result isConditionMet(Index index, ClusterState clusterState) { RoutingTable routingTable = clusterState.routingTable(); IndexRoutingTable indexRoutingTable = routingTable.index(index); + Result result; + switch (this.color) { + case GREEN: + result = waitForGreen(indexRoutingTable); + break; + case YELLOW: + result = waitForYellow(indexRoutingTable); + break; + case RED: + result = waitForRed(indexRoutingTable); + break; + default: + result = new Result(false, new Info("No index color match")); + break; + } + return result; + } + + private Result waitForRed(IndexRoutingTable indexRoutingTable) { + if (indexRoutingTable == null) { + return new Result(true, new Info("Index is red")); + } + return new Result(false, new Info("Index is not red")); + } + + private Result waitForYellow(IndexRoutingTable indexRoutingTable) { + if (indexRoutingTable == null) { + return new Result(false, new Info("index is red; no IndexRoutingTable")); + } + + boolean indexIsAtLeastYellow = indexRoutingTable.allPrimaryShardsActive(); + if (indexIsAtLeastYellow) { + return new Result(true, null); + } else { + return new Result(false, new Info("index is red; not all primary shards are active")); + } + } + + private Result waitForGreen(IndexRoutingTable indexRoutingTable) { if (indexRoutingTable == null) { return new Result(false, new Info("index is red; no IndexRoutingTable")); } boolean indexIsGreen = false; - if(indexRoutingTable.allPrimaryShardsActive()) { + if (indexRoutingTable.allPrimaryShardsActive()) { boolean replicaIndexIsGreen = false; for (ObjectCursor shardRouting : indexRoutingTable.getShards().values()) { replicaIndexIsGreen = shardRouting.value.replicaShards().stream().allMatch(ShardRouting::active); - if(!replicaIndexIsGreen) { + if (!replicaIndexIsGreen) { return new Result(false, new Info("index is yellow; not all replica shards are active")); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index c12f6e7459bd2..4f899f3b74355 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ilm; +import org.apache.lucene.codecs.Codec; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.Writeable.Reader; @@ -35,20 +36,21 @@ protected ForceMergeAction createTestInstance() { } static ForceMergeAction randomInstance() { - return new ForceMergeAction(randomIntBetween(1, 100), randomBoolean()); + return new ForceMergeAction(randomIntBetween(1, 100), createRandomCompressionSettings()); + } + + static Codec createRandomCompressionSettings() { + if(randomBoolean()) { + return Codec.getDefault(); + } + return Codec.forName(CodecService.BEST_COMPRESSION_CODEC); } @Override protected ForceMergeAction mutateInstance(ForceMergeAction instance) { int maxNumSegments = instance.getMaxNumSegments(); - boolean bestCompression = instance.isBestCompression(); - if(randomBoolean()) { - maxNumSegments = maxNumSegments + randomIntBetween(1, 10); - } - else { - bestCompression = !bestCompression; - } - return new ForceMergeAction(maxNumSegments, bestCompression); + maxNumSegments = maxNumSegments + randomIntBetween(1, 10); + return new ForceMergeAction(maxNumSegments, Codec.getDefault()); } @Override @@ -83,7 +85,7 @@ private void assertBestCompression(ForceMergeAction instance) { CloseIndexStep firstStep = (CloseIndexStep) steps.get(0); UpdateSettingsStep secondStep = (UpdateSettingsStep) steps.get(1); OpenIndexStep thirdStep = (OpenIndexStep) steps.get(2); - WaitForIndexGreenStep fourthStep = (WaitForIndexGreenStep) steps.get(3); + WaitForIndexColorStep fourthStep = (WaitForIndexColorStep) steps.get(3); ForceMergeStep fifthStep = (ForceMergeStep) steps.get(4); assertThat(firstStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, CloseIndexStep.NAME))); assertThat(firstStep.getNextStepKey(), equalTo(secondStep.getKey())); @@ -92,7 +94,7 @@ private void assertBestCompression(ForceMergeAction instance) { assertThat(secondStep.getNextStepKey(), equalTo(thirdStep.getKey())); assertThat(thirdStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, OpenIndexStep.NAME))); assertThat(thirdStep.getNextStepKey(), equalTo(fourthStep)); - assertThat(fourthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, WaitForIndexGreenStep.NAME))); + assertThat(fourthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, WaitForIndexColorStep.NAME))); assertThat(fourthStep.getNextStepKey(), equalTo(fifthStep)); assertThat(fifthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, ForceMergeStep.NAME))); assertThat(fifthStep.getNextStepKey(), equalTo(nextStepKey)); @@ -107,13 +109,13 @@ public void testMissingMaxNumSegments() throws IOException { } public void testInvalidNegativeSegmentNumber() { - Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0), false)); + Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0), Codec.getDefault())); assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer")); } public void testToSteps() { ForceMergeAction instance = createTestInstance(); - if (instance.isBestCompression()) { + if (CodecService.BEST_COMPRESSION_CODEC.equals(instance.getCodec().getName())) { assertBestCompression(instance); } else { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java index 49bc71c55971f..1469e40e6682d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ilm; +import org.apache.lucene.codecs.Codec; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; @@ -34,7 +35,7 @@ public class TimeseriesLifecycleTypeTests extends ESTestCase { private static final AllocateAction TEST_ALLOCATE_ACTION = new AllocateAction(2, Collections.singletonMap("node", "node1"),null, null); private static final DeleteAction TEST_DELETE_ACTION = new DeleteAction(); - private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1, false); + private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1, Codec.getDefault()); private static final RolloverAction TEST_ROLLOVER_ACTION = new RolloverAction(new ByteSizeValue(1), null, null); private static final ShrinkAction TEST_SHRINK_ACTION = new ShrinkAction(1); private static final ReadOnlyAction TEST_READ_ONLY_ACTION = new ReadOnlyAction(); @@ -492,7 +493,7 @@ private ConcurrentMap convertActionNamesToActions(Strin case DeleteAction.NAME: return new DeleteAction(); case ForceMergeAction.NAME: - return new ForceMergeAction(1, false); + return new ForceMergeAction(1, Codec.getDefault()); case ReadOnlyAction.NAME: return new ReadOnlyAction(); case RolloverAction.NAME: diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java similarity index 51% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java index 46fba4bcf1770..0baa5a830df82 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexGreenStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.routing.IndexRoutingTable; @@ -23,35 +24,48 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; -public class WaitForIndexGreenStepTests extends AbstractStepTestCase { +public class WaitForIndexColorStepTests extends AbstractStepTestCase { + + private static ClusterHealthStatus randomColor() { + String[] colors = new String[]{"green", "yellow", "red"}; + int randomColor = randomIntBetween(0, colors.length - 1); + return ClusterHealthStatus.fromString(colors[randomColor]); + } @Override - protected WaitForIndexGreenStep createRandomInstance() { + protected WaitForIndexColorStep createRandomInstance() { StepKey stepKey = randomStepKey(); StepKey nextStepKey = randomStepKey(); - return new WaitForIndexGreenStep(stepKey, nextStepKey); + ClusterHealthStatus color = randomColor(); + return new WaitForIndexColorStep(stepKey, nextStepKey, color); } @Override - protected WaitForIndexGreenStep mutateInstance(WaitForIndexGreenStep instance) { + protected WaitForIndexColorStep mutateInstance(WaitForIndexColorStep instance) { StepKey key = instance.getKey(); StepKey nextKey = instance.getNextStepKey(); - - if (randomBoolean()) { - key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); - } else { - nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + ClusterHealthStatus color = instance.getColor(); + + switch (between(0, 2)) { + case 0: + key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 1: + nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 2: + color = randomColor(); } - return new WaitForIndexGreenStep(key, nextKey); + return new WaitForIndexColorStep(key, nextKey, color); } @Override - protected WaitForIndexGreenStep copyInstance(WaitForIndexGreenStep instance) { - return new WaitForIndexGreenStep(instance.getKey(), instance.getNextStepKey()); + protected WaitForIndexColorStep copyInstance(WaitForIndexColorStep instance) { + return new WaitForIndexColorStep(instance.getKey(), instance.getNextStepKey(), instance.getColor()); } - public void testConditionMet() { + public void testConditionMetForGreen() { IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) .settings(settings(Version.CURRENT)) .numberOfShards(1) @@ -68,13 +82,13 @@ public void testConditionMet() { .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) .build(); - WaitForIndexGreenStep step = createRandomInstance(); + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); assertThat(result.isComplete(), is(true)); assertThat(result.getInfomationContext(), nullValue()); } - public void testConditionNotMet() { + public void testConditionNotMetForGreen() { IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) .settings(settings(Version.CURRENT)) .numberOfShards(1) @@ -91,47 +105,84 @@ public void testConditionNotMet() { .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) .build(); - WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); assertThat(result.isComplete(), is(false)); - WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext(); assertThat(info, notNullValue()); assertThat(info.getMessage(), equalTo("index is not green; not all shards are active")); } - public void testConditionNotMetWithYellow() { + public void testConditionNotMetNoIndexRoutingTable() { IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) .settings(settings(Version.CURRENT)) .numberOfShards(1) - .numberOfReplicas(2) + .numberOfReplicas(0) .build(); - ShardRouting shardRouting = - TestShardRouting.newShardRouting("test_index", 0, "1", true, ShardRoutingState.STARTED); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().build()) + .build(); - ShardRouting replicaShardRouting = - TestShardRouting.newShardRouting("test_index", 0, "2", false, ShardRoutingState.INITIALIZING); + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(false)); + WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext(); + assertThat(info, notNullValue()); + assertThat(info.getMessage(), equalTo("index is red; no IndexRoutingTable")); + } + + public void testConditionMetForYellow() { + IndexMetaData indexMetadata = IndexMetaData.builder("former-follower-index") + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + ShardRouting shardRouting = + TestShardRouting.newShardRouting("index2", 0, "1", true, ShardRoutingState.STARTED); IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex()) - .addShard(shardRouting) - .addShard(replicaShardRouting) + .addShard(shardRouting).build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metaData(MetaData.builder().put(indexMetadata, true).build()) + .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) + .build(); + + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); + ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(true)); + assertThat(result.getInfomationContext(), nullValue()); + } + + public void testConditionNotMetForYellow() { + IndexMetaData indexMetadata = IndexMetaData.builder("former-follower-index") + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) .build(); + ShardRouting shardRouting = + TestShardRouting.newShardRouting("index2", 0, "1", true, ShardRoutingState.INITIALIZING); + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex()) + .addShard(shardRouting).build(); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) .metaData(MetaData.builder().put(indexMetadata, true).build()) .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) .build(); - WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); assertThat(result.isComplete(), is(false)); - WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext(); assertThat(info, notNullValue()); - assertThat(info.getMessage(), equalTo("index is yellow; not all replica shards are active")); + assertThat(info.getMessage(), equalTo("index is red; not all primary shards are active")); } - public void testConditionNotMetNoIndexRoutingTable() { - IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5)) + public void testConditionNotMetNoIndexRoutingTableForYellow() { + IndexMetaData indexMetadata = IndexMetaData.builder("former-follower-index") .settings(settings(Version.CURRENT)) .numberOfShards(1) .numberOfReplicas(0) @@ -142,10 +193,10 @@ public void testConditionNotMetNoIndexRoutingTable() { .routingTable(RoutingTable.builder().build()) .build(); - WaitForIndexGreenStep step = new WaitForIndexGreenStep(randomStepKey(), randomStepKey()); + WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); assertThat(result.isComplete(), is(false)); - WaitForIndexGreenStep.Info info = (WaitForIndexGreenStep.Info) result.getInfomationContext(); + WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext(); assertThat(info, notNullValue()); assertThat(info.getMessage(), equalTo("index is red; no IndexRoutingTable")); } From 831c078d17fc17c1ea80814e14dc31e810f6c0e9 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Mon, 16 Dec 2019 20:34:58 -0800 Subject: [PATCH 225/686] Precommit check changes --- .../org/elasticsearch/xpack/core/ilm/ForceMergeAction.java | 7 ++++--- .../xpack/core/ilm/ForceMergeActionTests.java | 3 ++- .../xpack/ilm/TimeSeriesLifecycleActionsIT.java | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index 636f7a83e28c8..ef49d537684a8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -38,7 +38,7 @@ public class ForceMergeAction implements LifecycleAction { private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, a -> { int maxNumSegments = (int) a[0]; - Codec codec = a[1] != null ? Codec.forName((String)a[1]): Codec.getDefault(); + Codec codec = a[1] != null ? Codec.forName((String) a[1]) : Codec.getDefault(); return new ForceMergeAction(maxNumSegments, codec); }); @@ -68,7 +68,7 @@ public ForceMergeAction(StreamInput in) throws IOException { if (in.getVersion().onOrAfter(Version.V_8_0_0)) { this.codec = Codec.forName(in.readString()); } else { - this.codec = Codec.getDefault(); + this.codec = Codec.getDefault(); } } @@ -130,7 +130,8 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey, openKey, client, bestCompressionSettings); OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client); - WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey, forceMergeKey, ClusterHealthStatus.GREEN); + WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey, + forceMergeKey, ClusterHealthStatus.GREEN); ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, nextStepKey, client, maxNumSegments); return Arrays.asList(closeIndexStep, updateBestCompressionSettings, openIndexStep, waitForIndexGreenStep, forceMergeStep); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index 4f899f3b74355..ef88e461c0d13 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -109,7 +109,8 @@ public void testMissingMaxNumSegments() throws IOException { } public void testInvalidNegativeSegmentNumber() { - Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0), Codec.getDefault())); + Exception r = expectThrows(IllegalArgumentException.class, () -> new + ForceMergeAction(randomIntBetween(-10, 0), Codec.getDefault())); assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer")); } diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 2ee3de617c432..f1de3fe738820 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -10,6 +10,7 @@ import org.apache.http.entity.StringEntity; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; +import org.apache.lucene.codecs.Codec; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; @@ -411,7 +412,7 @@ public void testForceMergeAction() throws Exception { }; assertThat(numSegments.get(), greaterThan(1)); - createNewSingletonPolicy("warm", new ForceMergeAction(1, false)); + createNewSingletonPolicy("warm", new ForceMergeAction(1, Codec.getDefault())); updatePolicy(index, policy); assertBusy(() -> { @@ -1007,7 +1008,7 @@ private void createFullPolicy(TimeValue hotTime) throws IOException { hotActions.put(RolloverAction.NAME, new RolloverAction(null, null, 1L)); Map warmActions = new HashMap<>(); warmActions.put(SetPriorityAction.NAME, new SetPriorityAction(50)); - warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, false)); + warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, Codec.getDefault())); warmActions.put(AllocateAction.NAME, new AllocateAction(1, singletonMap("_name", "integTest-1,integTest-2"), null, null)); warmActions.put(ShrinkAction.NAME, new ShrinkAction(1)); Map coldActions = new HashMap<>(); From b2236bb014d81e21cadd530b0f7fd6285f677806 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 17 Dec 2019 08:54:29 +0100 Subject: [PATCH 226/686] Better Logging S3 Bulk Delete Failures (#50203) Unfortunately bulk delete exceptions don't show the individual delete errors when a bulk delete fails when you log them outright so I added this work-around to get the individual details to get useful logging. --- .../elasticsearch/repositories/s3/S3BlobContainer.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java index 6afb81da5e02c..9cb118d60086f 100644 --- a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java +++ b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java @@ -32,6 +32,9 @@ import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.UploadPartRequest; import com.amazonaws.services.s3.model.UploadPartResult; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.lucene.util.SetOnce; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.Nullable; @@ -61,6 +64,8 @@ class S3BlobContainer extends AbstractBlobContainer { + private static final Logger logger = LogManager.getLogger(S3BlobContainer.class); + /** * Maximum number of deletes in a {@link DeleteObjectsRequest}. * @see S3 Documentation. @@ -189,6 +194,10 @@ private void doDeleteBlobs(List blobNames, boolean relative) throws IOEx outstanding.removeAll(keysInRequest); outstanding.addAll( e.getErrors().stream().map(MultiObjectDeleteException.DeleteError::getKey).collect(Collectors.toSet())); + logger.warn( + () -> new ParameterizedMessage("Failed to delete some blobs {}", e.getErrors() + .stream().map(err -> "[" + err.getKey() + "][" + err.getCode() + "][" + err.getMessage() + "]") + .collect(Collectors.toList())), e); aex = ExceptionsHelper.useOrSuppress(aex, e); } catch (AmazonClientException e) { // The AWS client threw any unexpected exception and did not execute the request at all so we do not From bebdbdc8cd7f53f41e1400f4fc668c8fafe11d01 Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Tue, 17 Dec 2019 10:00:54 +0200 Subject: [PATCH 227/686] Handle NULL in ResultSet's getDate() method (#50184) --- .../xpack/sql/jdbc/JdbcResultSet.java | 10 ++-- .../xpack/sql/qa/jdbc/ResultSetTestCase.java | 52 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/JdbcResultSet.java b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/JdbcResultSet.java index 6002ef1496890..71d46ad66d942 100644 --- a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/JdbcResultSet.java +++ b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/JdbcResultSet.java @@ -259,6 +259,11 @@ public Date getDate(String columnLabel) throws SQLException { private Long dateTimeAsMillis(int columnIndex) throws SQLException { Object val = column(columnIndex); EsType type = columnType(columnIndex); + + if (val == null) { + return null; + } + try { // TODO: the B6 appendix of the jdbc spec does mention CHAR, VARCHAR, LONGVARCHAR, DATE, TIMESTAMP as supported // jdbc types that should be handled by getDate and getTime methods. From all of those we support VARCHAR and @@ -267,9 +272,6 @@ private Long dateTimeAsMillis(int columnIndex) throws SQLException { // the cursor can return an Integer if the date-since-epoch is small enough, XContentParser (Jackson) will // return the "smallest" data type for numbers when parsing // TODO: this should probably be handled server side - if (val == null) { - return null; - } return asDateTimeField(val, JdbcDateUtils::dateTimeAsMillisSinceEpoch, Function.identity()); } if (DATE == type) { @@ -278,7 +280,7 @@ private Long dateTimeAsMillis(int columnIndex) throws SQLException { if (TIME == type) { return timeAsMillisSinceEpoch(val.toString()); } - return val == null ? null : (Long) val; + return (Long) val; } catch (ClassCastException cce) { throw new SQLException( format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Long", val, type.getName()), cce); diff --git a/x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java b/x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java index b842193efc904..e59316ed66fac 100644 --- a/x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java +++ b/x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java @@ -1275,6 +1275,54 @@ public void testValidGetObjectCalls() throws Exception { assertTrue(results.getObject("test_boolean") instanceof Boolean); }); } + + public void testGettingNullValues() throws Exception { + String query = "SELECT CAST(NULL AS BOOLEAN) b, CAST(NULL AS TINYINT) t, CAST(NULL AS SMALLINT) s, CAST(NULL AS INTEGER) i," + + "CAST(NULL AS BIGINT) bi, CAST(NULL AS DOUBLE) d, CAST(NULL AS REAL) r, CAST(NULL AS FLOAT) f, CAST(NULL AS VARCHAR) v," + + "CAST(NULL AS DATE) dt, CAST(NULL AS TIME) tm, CAST(NULL AS TIMESTAMP) ts"; + doWithQuery(query, (results) -> { + results.next(); + + assertNull(results.getObject("b")); + assertFalse(results.getBoolean("b")); + + assertNull(results.getObject("t")); + assertEquals(0, results.getByte("t")); + + assertNull(results.getObject("s")); + assertEquals(0, results.getShort("s")); + + assertNull(results.getObject("i")); + assertEquals(0, results.getInt("i")); + + assertNull(results.getObject("bi")); + assertEquals(0, results.getLong("bi")); + + assertNull(results.getObject("d")); + assertEquals(0.0d, results.getDouble("d"), 0d); + + assertNull(results.getObject("r")); + assertEquals(0.0f, results.getFloat("r"), 0f); + + assertNull(results.getObject("f")); + assertEquals(0.0f, results.getFloat("f"), 0f); + + assertNull(results.getObject("v")); + assertNull(results.getString("v")); + + assertNull(results.getObject("dt")); + assertNull(results.getDate("dt")); + assertNull(results.getDate("dt", randomCalendar())); + + assertNull(results.getObject("tm")); + assertNull(results.getTime("tm")); + assertNull(results.getTime("tm", randomCalendar())); + + assertNull(results.getObject("ts")); + assertNull(results.getTimestamp("ts")); + assertNull(results.getTimestamp("ts", randomCalendar())); + }); + } /* * Checks StackOverflowError fix for https://github.com/elastic/elasticsearch/pull/31735 @@ -1841,4 +1889,8 @@ private String asDateString(long millis) { private ZoneId getZoneFromOffset(Long randomLongDate) { return ZoneId.of(ZoneId.of(timeZoneId).getRules().getOffset(Instant.ofEpochMilli(randomLongDate)).toString()); } + + private Calendar randomCalendar() { + return Calendar.getInstance(randomTimeZone(), Locale.ROOT); + } } From 812d16a80d417c65e957e31b202d45d33c6e7915 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 17 Dec 2019 09:42:02 +0100 Subject: [PATCH 228/686] Fix Index Deletion During Partial Snapshot Create (#50234) * Fix Index Deletion During Partial Snapshot Create We can simply filter out shard generation updates for indices that were removed from the cluster state concurrently to fix index deletes during partial snapshots as that completely removes any reference to those shards from the snapshot. Follow up to #50202 Closes #50200 --- .../repositories/ShardGenerations.java | 7 +++ .../snapshots/SnapshotsService.java | 24 +++++++--- .../snapshots/SnapshotResiliencyTests.java | 46 +++++++++++++++++-- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/ShardGenerations.java b/server/src/main/java/org/elasticsearch/repositories/ShardGenerations.java index 6351d5e2f2bf0..8b7f799d0e7c2 100644 --- a/server/src/main/java/org/elasticsearch/repositories/ShardGenerations.java +++ b/server/src/main/java/org/elasticsearch/repositories/ShardGenerations.java @@ -54,6 +54,13 @@ private ShardGenerations(Map> shardGenerations) { this.shardGenerations = shardGenerations; } + /** + * Returns the total number of shards tracked by this instance. + */ + public int totalShards() { + return shardGenerations.values().stream().mapToInt(List::size).sum(); + } + /** * Returns all indices for which shard generations are tracked. * diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index 7c638bc332edc..cfb2112b4044d 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -568,16 +568,17 @@ public void onNoLongerMaster() { private void cleanupAfterError(Exception exception) { threadPool.generic().execute(() -> { if (snapshotCreated) { + final MetaData metaData = clusterService.state().metaData(); repositoriesService.repository(snapshot.snapshot().getRepository()) .finalizeSnapshot(snapshot.snapshot().getSnapshotId(), - buildGenerations(snapshot), + buildGenerations(snapshot, metaData), snapshot.startTime(), ExceptionsHelper.stackTrace(exception), 0, Collections.emptyList(), snapshot.repositoryStateId(), snapshot.includeGlobalState(), - metaDataForSnapshot(snapshot, clusterService.state().metaData()), + metaDataForSnapshot(snapshot, metaData), snapshot.userMetadata(), snapshot.useShardGenerations(), ActionListener.runAfter(ActionListener.wrap(ignored -> { @@ -593,11 +594,21 @@ private void cleanupAfterError(Exception exception) { } } - private static ShardGenerations buildGenerations(SnapshotsInProgress.Entry snapshot) { + private static ShardGenerations buildGenerations(SnapshotsInProgress.Entry snapshot, MetaData metaData) { ShardGenerations.Builder builder = ShardGenerations.builder(); final Map indexLookup = new HashMap<>(); snapshot.indices().forEach(idx -> indexLookup.put(idx.getName(), idx)); - snapshot.shards().forEach(c -> builder.put(indexLookup.get(c.key.getIndexName()), c.key.id(), c.value.generation())); + snapshot.shards().forEach(c -> { + if (metaData.index(c.key.getIndex()) == null) { + assert snapshot.partial() : + "Index [" + c.key.getIndex() + "] was deleted during a snapshot but snapshot was not partial."; + return; + } + final IndexId indexId = indexLookup.get(c.key.getIndexName()); + if (indexId != null) { + builder.put(indexId, c.key.id(), c.value.generation()); + } + }); return builder.build(); } @@ -1032,12 +1043,13 @@ protected void doRun() { shardFailures.add(new SnapshotShardFailure(status.nodeId(), shardId, status.reason())); } } + final ShardGenerations shardGenerations = buildGenerations(entry, metaData); repository.finalizeSnapshot( snapshot.getSnapshotId(), - buildGenerations(entry), + shardGenerations, entry.startTime(), failure, - entry.shards().size(), + entry.partial() ? shardGenerations.totalShards() : entry.shards().size(), unmodifiableList(shardFailures), entry.repositoryStateId(), entry.includeGlobalState(), diff --git a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java index a0e7e51098681..5fd6624d1d128 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java @@ -21,6 +21,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -143,6 +144,7 @@ import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.gateway.MetaStateService; import org.elasticsearch.gateway.TransportNodesListGatewayStartedShards; +import org.elasticsearch.index.Index; import org.elasticsearch.index.analysis.AnalysisRegistry; import org.elasticsearch.index.seqno.GlobalCheckpointSyncAction; import org.elasticsearch.index.seqno.RetentionLeaseSyncer; @@ -211,6 +213,7 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.mockito.Mockito.mock; @@ -503,7 +506,7 @@ public void testConcurrentSnapshotCreateAndDeleteOther() { } } - public void testConcurrentSnapshotDeleteAndDeleteIndex() { + public void testConcurrentSnapshotDeleteAndDeleteIndex() throws IOException { setupTestCluster(randomFrom(1, 3, 5), randomIntBetween(2, 10)); String repoName = "repo"; @@ -514,11 +517,13 @@ public void testConcurrentSnapshotDeleteAndDeleteIndex() { testClusterNodes.currentMaster(testClusterNodes.nodes.values().iterator().next().clusterService.state()); final StepListener> createIndicesListener = new StepListener<>(); + final int indices = randomIntBetween(5, 20); + final SetOnce firstIndex = new SetOnce<>(); continueOrDie(createRepoAndIndex(repoName, index, 1), createIndexResponse -> { + firstIndex.set(masterNode.clusterService.state().metaData().index(index).getIndex()); // create a few more indices to make it more likely that the subsequent index delete operation happens before snapshot // finalization - final int indices = randomIntBetween(5, 20); final GroupedActionListener listener = new GroupedActionListener<>(createIndicesListener, indices); for (int i = 0; i < indices; ++i) { client().admin().indices().create(new CreateIndexRequest("index-" + i), listener); @@ -527,23 +532,54 @@ public void testConcurrentSnapshotDeleteAndDeleteIndex() { final StepListener createSnapshotResponseStepListener = new StepListener<>(); + final boolean partialSnapshot = randomBoolean(); + continueOrDie(createIndicesListener, createIndexResponses -> client().admin().cluster().prepareCreateSnapshot(repoName, snapshotName).setWaitForCompletion(false) - .execute(createSnapshotResponseStepListener)); + .setPartial(partialSnapshot).execute(createSnapshotResponseStepListener)); continueOrDie(createSnapshotResponseStepListener, - createSnapshotResponse -> client().admin().indices().delete(new DeleteIndexRequest(index), noopListener())); + createSnapshotResponse -> client().admin().indices().delete(new DeleteIndexRequest(index), new ActionListener<>() { + @Override + public void onResponse(AcknowledgedResponse acknowledgedResponse) { + if (partialSnapshot) { + // Recreate index by the same name to test that we don't snapshot conflicting metadata in this scenario + client().admin().indices().create(new CreateIndexRequest(index), noopListener()); + } + } + + @Override + public void onFailure(Exception e) { + if (partialSnapshot) { + throw new AssertionError("Delete index should always work during partial snapshots", e); + } + } + })); deterministicTaskQueue.runAllRunnableTasks(); SnapshotsInProgress finalSnapshotsInProgress = masterNode.clusterService.state().custom(SnapshotsInProgress.TYPE); assertFalse(finalSnapshotsInProgress.entries().stream().anyMatch(entry -> entry.state().completed() == false)); final Repository repository = masterNode.repositoriesService.repository(repoName); - Collection snapshotIds = getRepositoryData(repository).getSnapshotIds(); + final RepositoryData repositoryData = getRepositoryData(repository); + Collection snapshotIds = repositoryData.getSnapshotIds(); assertThat(snapshotIds, hasSize(1)); final SnapshotInfo snapshotInfo = repository.getSnapshotInfo(snapshotIds.iterator().next()); assertEquals(SnapshotState.SUCCESS, snapshotInfo.state()); + if (partialSnapshot) { + // Single shard for each index so we either get all indices or all except for the deleted index + assertThat(snapshotInfo.successfulShards(), either(is(indices + 1)).or(is(indices))); + if (snapshotInfo.successfulShards() == indices + 1) { + final IndexMetaData indexMetaData = + repository.getSnapshotIndexMetaData(snapshotInfo.snapshotId(), repositoryData.resolveIndexId(index)); + // Make sure we snapshotted the metadata of this index and not the recreated version + assertEquals(indexMetaData.getIndex(), firstIndex.get()); + } + } else { + // Index delete must be blocked for non-partial snapshots and we get a snapshot for every index + assertEquals(snapshotInfo.successfulShards(), indices + 1); + } assertEquals(0, snapshotInfo.failedShards()); } From 7f6644ee2b960376263c820ac810452e6754ee32 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 17 Dec 2019 09:45:52 +0100 Subject: [PATCH 229/686] Use ClusterState as Consistency Source for Snapshot Repositories (#49060) Follow up to #49729 This change removes falling back to listing out the repository contents to find the latest `index-N` in write-mounted blob store repositories. This saves 2-3 list operations on each snapshot create and delete operation. Also it makes all the snapshot status APIs cheaper (and faster) by saving one list operation there as well in many cases. This removes the resiliency to concurrent modifications of the repository as a result and puts a repository in a `corrupted` state in case loading `RepositoryData` failed from the assumed generation. --- .../repositories/RepositoryData.java | 5 + .../blobstore/BlobStoreRepository.java | 215 ++++++++++++--- .../BlobStoreRepositoryRestoreTests.java | 8 +- .../CorruptedBlobStoreRepositoryIT.java | 247 ++++++++++++++++++ ...ckEventuallyConsistentRepositoryTests.java | 2 + .../AbstractThirdPartyRepositoryTestCase.java | 3 +- .../blobstore/BlobStoreTestUtil.java | 10 +- .../SourceOnlySnapshotShardTests.java | 9 +- 8 files changed, 449 insertions(+), 50 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/snapshots/CorruptedBlobStoreRepositoryIT.java diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java b/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java index 357268fa051e0..63e2957aacc78 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java @@ -58,6 +58,11 @@ public final class RepositoryData { */ public static final long UNKNOWN_REPO_GEN = -2L; + /** + * The generation value indicating that the repository generation could not be determined. + */ + public static final long CORRUPTED_REPO_GEN = -3L; + /** * An instance initialized for an empty repository. */ diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index b56a22e99845b..b9016394ca514 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -31,6 +31,7 @@ import org.apache.lucene.store.IndexOutput; import org.apache.lucene.store.RateLimiter; import org.apache.lucene.util.SetOnce; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.StepListener; @@ -184,6 +185,14 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp */ public static final Setting COMPRESS_SETTING = Setting.boolSetting("compress", true, Setting.Property.NodeScope); + /** + * When set to {@code true}, {@link #bestEffortConsistency} will be set to {@code true} and concurrent modifications of the repository + * contents will not result in the repository being marked as corrupted. + * Note: This setting is intended as a backwards compatibility solution for 7.x and will go away in 8. + */ + public static final Setting ALLOW_CONCURRENT_MODIFICATION = + Setting.boolSetting("allow_concurrent_modifications", false, Setting.Property.Deprecated); + private final boolean compress; private final RateLimiter snapshotRateLimiter; @@ -216,6 +225,34 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp private final ClusterService clusterService; + /** + * Flag that is set to {@code true} if this instance is started with {@link #metadata} that has a higher value for + * {@link RepositoryMetaData#pendingGeneration()} than for {@link RepositoryMetaData#generation()} indicating a full cluster restart + * potentially accounting for the the last {@code index-N} write in the cluster state. + * Note: While it is true that this value could also be set to {@code true} for an instance on a node that is just joining the cluster + * during a new {@code index-N} write, this does not present a problem. The node will still load the correct {@link RepositoryData} in + * all cases and simply do a redundant listing of the repository contents if it tries to load {@link RepositoryData} and falls back + * to {@link #latestIndexBlobId()} to validate the value of {@link RepositoryMetaData#generation()}. + */ + private boolean uncleanStart; + + /** + * This flag indicates that the repository can not exclusively rely on the value stored in {@link #latestKnownRepoGen} to determine the + * latest repository generation but must inspect its physical contents as well via {@link #latestIndexBlobId()}. + * This flag is set in the following situations: + *

      + *
    • All repositories that are read-only, i.e. for which {@link #isReadOnly()} returns {@code true} because there are no + * guarantees that another cluster is not writing to the repository at the same time
    • + *
    • The node finds itself in a mixed-version cluster containing nodes older than + * {@link RepositoryMetaData#REPO_GEN_IN_CS_VERSION} where the master node does not update the value of + * {@link RepositoryMetaData#generation()} when writing a new {@code index-N} blob
    • + *
    • The value of {@link RepositoryMetaData#generation()} for this repository is {@link RepositoryData#UNKNOWN_REPO_GEN} + * indicating that no consistent repository generation is tracked in the cluster state yet.
    • + *
    • The {@link #uncleanStart} flag is set to {@code true}
    • + *
    + */ + private volatile boolean bestEffortConsistency; + /** * Constructs new BlobStoreRepository * @param metadata The metadata for this repository including name and settings @@ -249,6 +286,8 @@ protected BlobStoreRepository( @Override protected void doStart() { + uncleanStart = metadata.pendingGeneration() > RepositoryData.EMPTY_REPO_GEN && + metadata.generation() != metadata.pendingGeneration(); ByteSizeValue chunkSize = chunkSize(); if (chunkSize != null && chunkSize.getBytes() <= 0) { throw new IllegalArgumentException("the chunk size cannot be negative: [" + chunkSize + "]"); @@ -279,29 +318,42 @@ protected void doClose() { // #latestKnownRepoGen if a newer than currently known generation is found @Override public void updateState(ClusterState state) { - if (readOnly) { + metadata = getRepoMetaData(state); + uncleanStart = uncleanStart && metadata.generation() != metadata.pendingGeneration(); + bestEffortConsistency = uncleanStart || isReadOnly() + || state.nodes().getMinNodeVersion().before(RepositoryMetaData.REPO_GEN_IN_CS_VERSION) + || metadata.generation() == RepositoryData.UNKNOWN_REPO_GEN || ALLOW_CONCURRENT_MODIFICATION.get(metadata.settings()); + if (isReadOnly()) { // No need to waste cycles, no operations can run against a read-only repository return; } - long bestGenerationFromCS = RepositoryData.EMPTY_REPO_GEN; - final SnapshotsInProgress snapshotsInProgress = state.custom(SnapshotsInProgress.TYPE); - if (snapshotsInProgress != null) { - bestGenerationFromCS = bestGeneration(snapshotsInProgress.entries()); - } - final SnapshotDeletionsInProgress deletionsInProgress = state.custom(SnapshotDeletionsInProgress.TYPE); - // Don't use generation from the delete task if we already found a generation for an in progress snapshot. - // In this case, the generation points at the generation the repo will be in after the snapshot finishes so it may not yet exist - if (bestGenerationFromCS == RepositoryData.EMPTY_REPO_GEN && deletionsInProgress != null) { - bestGenerationFromCS = bestGeneration(deletionsInProgress.getEntries()); - } - final RepositoryCleanupInProgress cleanupInProgress = state.custom(RepositoryCleanupInProgress.TYPE); - if (bestGenerationFromCS == RepositoryData.EMPTY_REPO_GEN && cleanupInProgress != null) { - bestGenerationFromCS = bestGeneration(cleanupInProgress.entries()); + if (bestEffortConsistency) { + long bestGenerationFromCS = RepositoryData.EMPTY_REPO_GEN; + final SnapshotsInProgress snapshotsInProgress = state.custom(SnapshotsInProgress.TYPE); + if (snapshotsInProgress != null) { + bestGenerationFromCS = bestGeneration(snapshotsInProgress.entries()); + } + final SnapshotDeletionsInProgress deletionsInProgress = state.custom(SnapshotDeletionsInProgress.TYPE); + // Don't use generation from the delete task if we already found a generation for an in progress snapshot. + // In this case, the generation points at the generation the repo will be in after the snapshot finishes so it may not yet + // exist + if (bestGenerationFromCS == RepositoryData.EMPTY_REPO_GEN && deletionsInProgress != null) { + bestGenerationFromCS = bestGeneration(deletionsInProgress.getEntries()); + } + final RepositoryCleanupInProgress cleanupInProgress = state.custom(RepositoryCleanupInProgress.TYPE); + if (bestGenerationFromCS == RepositoryData.EMPTY_REPO_GEN && cleanupInProgress != null) { + bestGenerationFromCS = bestGeneration(cleanupInProgress.entries()); + } + final long finalBestGen = Math.max(bestGenerationFromCS, metadata.generation()); + latestKnownRepoGen.updateAndGet(known -> Math.max(known, finalBestGen)); + } else { + final long previousBest = latestKnownRepoGen.getAndSet(metadata.generation()); + if (previousBest != metadata.generation()) { + assert metadata.generation() == RepositoryData.CORRUPTED_REPO_GEN || previousBest < metadata.generation() : + "Illegal move from repository generation [" + previousBest + "] to generation [" + metadata.generation() + "]"; + logger.debug("Updated repository generation from [{}] to [{}]", previousBest, metadata.generation()); + } } - - metadata = getRepoMetaData(state); - final long finalBestGen = Math.max(bestGenerationFromCS, metadata.generation()); - latestKnownRepoGen.updateAndGet(known -> Math.max(known, finalBestGen)); } private long bestGeneration(Collection operations) { @@ -446,7 +498,12 @@ public void deleteSnapshot(SnapshotId snapshotId, long repositoryStateId, boolea */ private RepositoryData safeRepositoryData(long repositoryStateId, Map rootBlobs) { final long generation = latestGeneration(rootBlobs.keySet()); - final long genToLoad = latestKnownRepoGen.updateAndGet(known -> Math.max(known, repositoryStateId)); + final long genToLoad; + if (bestEffortConsistency) { + genToLoad = latestKnownRepoGen.updateAndGet(known -> Math.max(known, repositoryStateId)); + } else { + genToLoad = latestKnownRepoGen.get(); + } if (genToLoad > generation) { // It's always a possibility to not see the latest index-N in the listing here on an eventually consistent blob store, just // debug log it. Any blobs leaked as a result of an inconsistent listing here will be cleaned up in a subsequent cleanup or @@ -983,36 +1040,106 @@ public void endVerification(String seed) { // Tracks the latest known repository generation in a best-effort way to detect inconsistent listing of root level index-N blobs // and concurrent modifications. - private final AtomicLong latestKnownRepoGen = new AtomicLong(RepositoryData.EMPTY_REPO_GEN); + private final AtomicLong latestKnownRepoGen = new AtomicLong(RepositoryData.UNKNOWN_REPO_GEN); @Override public void getRepositoryData(ActionListener listener) { - ActionListener.completeWith(listener, () -> { - // Retry loading RepositoryData in a loop in case we run into concurrent modifications of the repository. - while (true) { + if (latestKnownRepoGen.get() == RepositoryData.CORRUPTED_REPO_GEN) { + listener.onFailure(corruptedStateException(null)); + return; + } + // Retry loading RepositoryData in a loop in case we run into concurrent modifications of the repository. + while (true) { + final long genToLoad; + if (bestEffortConsistency) { + // We're only using #latestKnownRepoGen as a hint in this mode and listing repo contents as a secondary way of trying + // to find a higher generation final long generation; try { generation = latestIndexBlobId(); } catch (IOException ioe) { throw new RepositoryException(metadata.name(), "Could not determine repository generation from root blobs", ioe); } - final long genToLoad = latestKnownRepoGen.updateAndGet(known -> Math.max(known, generation)); + genToLoad = latestKnownRepoGen.updateAndGet(known -> Math.max(known, generation)); if (genToLoad > generation) { logger.info("Determined repository generation [" + generation + "] from repository contents but correct generation must be at least [" + genToLoad + "]"); } - try { - return getRepositoryData(genToLoad); - } catch (RepositoryException e) { - if (genToLoad != latestKnownRepoGen.get()) { - logger.warn("Failed to load repository data generation [" + genToLoad + - "] because a concurrent operation moved the current generation to [" + latestKnownRepoGen.get() + "]", e); - continue; - } + } else { + // We only rely on the generation tracked in #latestKnownRepoGen which is exclusively updated from the cluster state + genToLoad = latestKnownRepoGen.get(); + } + try { + listener.onResponse(getRepositoryData(genToLoad)); + return; + } catch (RepositoryException e) { + if (genToLoad != latestKnownRepoGen.get()) { + logger.warn("Failed to load repository data generation [" + genToLoad + + "] because a concurrent operation moved the current generation to [" + latestKnownRepoGen.get() + "]", e); + continue; + } + if (bestEffortConsistency == false && ExceptionsHelper.unwrap(e, NoSuchFileException.class) != null) { + // We did not find the expected index-N even though the cluster state continues to point at the missing value + // of N so we mark this repository as corrupted. + markRepoCorrupted(genToLoad, e, + ActionListener.wrap(v -> listener.onFailure(corruptedStateException(e)), listener::onFailure)); + return; + } else { throw e; } } - }); + } + } + + private RepositoryException corruptedStateException(@Nullable Exception cause) { + return new RepositoryException(metadata.name(), + "Could not read repository data because the contents of the repository do not match its " + + "expected state. This is likely the result of either concurrently modifying the contents of the " + + "repository by a process other than this cluster or an issue with the repository's underlying" + + "storage. The repository has been disabled to prevent corrupting its contents. To re-enable it " + + "and continue using it please remove the repository from the cluster and add it again to make " + + "the cluster recover the known state of the repository from its physical contents.", cause); + } + + /** + * Marks the repository as corrupted. This puts the repository in a state where its tracked value for + * {@link RepositoryMetaData#pendingGeneration()} is unchanged while its value for {@link RepositoryMetaData#generation()} is set to + * {@link RepositoryData#CORRUPTED_REPO_GEN}. In this state, the repository can not be used any longer and must be removed and + * recreated after the problem that lead to it being marked as corrupted has been fixed. + * + * @param corruptedGeneration generation that failed to load because the index file was not found but that should have loaded + * @param originalException exception that lead to the failing to load the {@code index-N} blob + * @param listener listener to invoke once done + */ + private void markRepoCorrupted(long corruptedGeneration, Exception originalException, ActionListener listener) { + assert corruptedGeneration != RepositoryData.UNKNOWN_REPO_GEN; + assert bestEffortConsistency == false; + clusterService.submitStateUpdateTask("mark repository corrupted [" + metadata.name() + "][" + corruptedGeneration + "]", + new ClusterStateUpdateTask() { + @Override + public ClusterState execute(ClusterState currentState) { + final RepositoriesMetaData state = currentState.metaData().custom(RepositoriesMetaData.TYPE); + final RepositoryMetaData repoState = state.repository(metadata.name()); + if (repoState.generation() != corruptedGeneration) { + throw new IllegalStateException("Tried to mark repo generation [" + corruptedGeneration + + "] as corrupted but its state concurrently changed to [" + repoState + "]"); + } + return ClusterState.builder(currentState).metaData(MetaData.builder(currentState.metaData()).putCustom( + RepositoriesMetaData.TYPE, state.withUpdatedGeneration( + metadata.name(), RepositoryData.CORRUPTED_REPO_GEN, repoState.pendingGeneration())).build()).build(); + } + + @Override + public void onFailure(String source, Exception e) { + listener.onFailure(new RepositoryException(metadata.name(), "Failed marking repository state as corrupted", + ExceptionsHelper.useOrSuppress(e, originalException))); + } + + @Override + public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { + listener.onResponse(null); + } + }); } private RepositoryData getRepositoryData(long indexGen) { @@ -1029,11 +1156,13 @@ private RepositoryData getRepositoryData(long indexGen) { return RepositoryData.snapshotsFromXContent(parser, indexGen); } } catch (IOException ioe) { - // If we fail to load the generation we tracked in latestKnownRepoGen we reset it. - // This is done as a fail-safe in case a user manually deletes the contents of the repository in which case subsequent - // operations must start from the EMPTY_REPO_GEN again - if (latestKnownRepoGen.compareAndSet(indexGen, RepositoryData.EMPTY_REPO_GEN)) { - logger.warn("Resetting repository generation tracker because we failed to read generation [" + indexGen + "]", ioe); + if (bestEffortConsistency) { + // If we fail to load the generation we tracked in latestKnownRepoGen we reset it. + // This is done as a fail-safe in case a user manually deletes the contents of the repository in which case subsequent + // operations must start from the EMPTY_REPO_GEN again + if (latestKnownRepoGen.compareAndSet(indexGen, RepositoryData.EMPTY_REPO_GEN)) { + logger.warn("Resetting repository generation tracker because we failed to read generation [" + indexGen + "]", ioe); + } } throw new RepositoryException(metadata.name(), "could not read repository data from index blob", ioe); } @@ -1085,13 +1214,12 @@ public ClusterState execute(ClusterState currentState) { final RepositoryMetaData meta = getRepoMetaData(currentState); final String repoName = metadata.name(); final long genInState = meta.generation(); - // TODO: Remove all usages of this variable, instead initialize the generation when loading RepositoryData - final boolean uninitializedMeta = meta.generation() == RepositoryData.UNKNOWN_REPO_GEN; + final boolean uninitializedMeta = meta.generation() == RepositoryData.UNKNOWN_REPO_GEN || bestEffortConsistency; if (uninitializedMeta == false && meta.pendingGeneration() != genInState) { logger.info("Trying to write new repository data over unfinished write, repo [{}] is at " + "safe generation [{}] and pending generation [{}]", meta.name(), genInState, meta.pendingGeneration()); } - assert expectedGen == RepositoryData.EMPTY_REPO_GEN || RepositoryData.UNKNOWN_REPO_GEN == meta.generation() + assert expectedGen == RepositoryData.EMPTY_REPO_GEN || uninitializedMeta || expectedGen == meta.generation() : "Expected non-empty generation [" + expectedGen + "] does not match generation tracked in [" + meta + "]"; // If we run into the empty repo generation for the expected gen, the repo is assumed to have been cleared of @@ -1102,7 +1230,8 @@ public ClusterState execute(ClusterState currentState) { // even if a repository has been manually cleared of all contents we will never reuse the same repository generation. // This is motivated by the consistency behavior the S3 based blob repository implementation has to support which does // not offer any consistency guarantees when it comes to overwriting the same blob name with different content. - newGen = uninitializedMeta ? expectedGen + 1: metadata.pendingGeneration() + 1; + final long nextPendingGen = metadata.pendingGeneration() + 1; + newGen = uninitializedMeta ? Math.max(expectedGen + 1, nextPendingGen) : nextPendingGen; assert newGen > latestKnownRepoGen.get() : "Attempted new generation [" + newGen + "] must be larger than latest known generation [" + latestKnownRepoGen.get() + "]"; return ClusterState.builder(currentState).metaData(MetaData.builder(currentState.getMetaData()) diff --git a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java index 432091b81e1ec..40e17a81be40f 100644 --- a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryRestoreTests.java @@ -27,6 +27,7 @@ import org.elasticsearch.cluster.routing.RecoverySource; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.routing.ShardRoutingHelper; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.internal.io.IOUtils; @@ -193,13 +194,16 @@ public void testSnapshotWithConflictingName() throws IOException { private Repository createRepository() { Settings settings = Settings.builder().put("location", randomAlphaOfLength(10)).build(); RepositoryMetaData repositoryMetaData = new RepositoryMetaData(randomAlphaOfLength(10), FsRepository.TYPE, settings); - final FsRepository repository = new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), - BlobStoreTestUtil.mockClusterService(repositoryMetaData)) { + final ClusterService clusterService = BlobStoreTestUtil.mockClusterService(repositoryMetaData); + final FsRepository repository = new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), clusterService) { @Override protected void assertSnapshotOrGenericThread() { // eliminate thread name check as we create repo manually } }; + clusterService.addStateApplier(event -> repository.updateState(event.state())); + // Apply state once to initialize repo properly like RepositoriesService would + repository.updateState(clusterService.state()); repository.start(); return repository; } diff --git a/server/src/test/java/org/elasticsearch/snapshots/CorruptedBlobStoreRepositoryIT.java b/server/src/test/java/org/elasticsearch/snapshots/CorruptedBlobStoreRepositoryIT.java new file mode 100644 index 0000000000000..2a860eda3f972 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/snapshots/CorruptedBlobStoreRepositoryIT.java @@ -0,0 +1,247 @@ +/* + * 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. + */ +package org.elasticsearch.snapshots; + +import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateUpdateTask; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.metadata.RepositoriesMetaData; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeUnit; +import org.elasticsearch.repositories.RepositoriesService; +import org.elasticsearch.repositories.Repository; +import org.elasticsearch.repositories.RepositoryData; +import org.elasticsearch.repositories.RepositoryException; +import org.elasticsearch.repositories.blobstore.BlobStoreRepository; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; + +public class CorruptedBlobStoreRepositoryIT extends AbstractSnapshotIntegTestCase { + + public void testConcurrentlyChangeRepositoryContents() throws Exception { + Client client = client(); + + Path repo = randomRepoPath(); + final String repoName = "test-repo"; + logger.info("--> creating repository at {}", repo.toAbsolutePath()); + assertAcked(client.admin().cluster().preparePutRepository(repoName) + .setType("fs").setSettings(Settings.builder() + .put("location", repo) + .put("compress", false) + .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); + + createIndex("test-idx-1", "test-idx-2"); + logger.info("--> indexing some data"); + indexRandom(true, + client().prepareIndex("test-idx-1").setSource("foo", "bar"), + client().prepareIndex("test-idx-2").setSource("foo", "bar")); + + final String snapshot = "test-snap"; + + logger.info("--> creating snapshot"); + CreateSnapshotResponse createSnapshotResponse = client.admin().cluster().prepareCreateSnapshot(repoName, snapshot) + .setWaitForCompletion(true).setIndices("test-idx-*").get(); + assertThat(createSnapshotResponse.getSnapshotInfo().successfulShards(), greaterThan(0)); + assertThat(createSnapshotResponse.getSnapshotInfo().successfulShards(), + equalTo(createSnapshotResponse.getSnapshotInfo().totalShards())); + + logger.info("--> move index-N blob to next generation"); + final RepositoryData repositoryData = + getRepositoryData(internalCluster().getMasterNodeInstance(RepositoriesService.class).repository(repoName)); + Files.move(repo.resolve("index-" + repositoryData.getGenId()), repo.resolve("index-" + (repositoryData.getGenId() + 1))); + + assertRepositoryBlocked(client, repoName, snapshot); + + if (randomBoolean()) { + logger.info("--> move index-N blob back to initial generation"); + Files.move(repo.resolve("index-" + (repositoryData.getGenId() + 1)), repo.resolve("index-" + repositoryData.getGenId())); + + logger.info("--> verify repository remains blocked"); + assertRepositoryBlocked(client, repoName, snapshot); + } + + logger.info("--> remove repository"); + assertAcked(client.admin().cluster().prepareDeleteRepository(repoName)); + + logger.info("--> recreate repository"); + assertAcked(client.admin().cluster().preparePutRepository(repoName) + .setType("fs").setSettings(Settings.builder() + .put("location", repo) + .put("compress", false) + .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); + + logger.info("--> delete snapshot"); + client.admin().cluster().prepareDeleteSnapshot(repoName, snapshot).get(); + + logger.info("--> make sure snapshot doesn't exist"); + expectThrows(SnapshotMissingException.class, () -> client.admin().cluster().prepareGetSnapshots(repoName) + .addSnapshots(snapshot).get().getSnapshots(repoName)); + } + + public void testConcurrentlyChangeRepositoryContentsInBwCMode() throws Exception { + Client client = client(); + + Path repo = randomRepoPath(); + final String repoName = "test-repo"; + logger.info("--> creating repository at {}", repo.toAbsolutePath()); + assertAcked(client.admin().cluster().preparePutRepository(repoName) + .setType("fs").setSettings(Settings.builder() + .put("location", repo) + .put("compress", false) + .put(BlobStoreRepository.ALLOW_CONCURRENT_MODIFICATION.getKey(), true) + .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); + + createIndex("test-idx-1", "test-idx-2"); + logger.info("--> indexing some data"); + indexRandom(true, + client().prepareIndex("test-idx-1").setSource("foo", "bar"), + client().prepareIndex("test-idx-2").setSource("foo", "bar")); + + final String snapshot = "test-snap"; + + logger.info("--> creating snapshot"); + CreateSnapshotResponse createSnapshotResponse = client.admin().cluster().prepareCreateSnapshot(repoName, snapshot) + .setWaitForCompletion(true).setIndices("test-idx-*").get(); + assertThat(createSnapshotResponse.getSnapshotInfo().successfulShards(), greaterThan(0)); + assertThat(createSnapshotResponse.getSnapshotInfo().successfulShards(), + equalTo(createSnapshotResponse.getSnapshotInfo().totalShards())); + + final Repository repository = internalCluster().getMasterNodeInstance(RepositoriesService.class).repository(repoName); + + logger.info("--> move index-N blob to next generation"); + final RepositoryData repositoryData = getRepositoryData(repository); + final long beforeMoveGen = repositoryData.getGenId(); + Files.move(repo.resolve("index-" + beforeMoveGen), repo.resolve("index-" + (beforeMoveGen + 1))); + + logger.info("--> verify index-N blob is found at the new location"); + assertThat(getRepositoryData(repository).getGenId(), is(beforeMoveGen + 1)); + + logger.info("--> delete snapshot"); + client.admin().cluster().prepareDeleteSnapshot(repoName, snapshot).get(); + + logger.info("--> verify index-N blob is found at the expected location"); + assertThat(getRepositoryData(repository).getGenId(), is(beforeMoveGen + 2)); + + logger.info("--> make sure snapshot doesn't exist"); + expectThrows(SnapshotMissingException.class, () -> client.admin().cluster().prepareGetSnapshots(repoName) + .addSnapshots(snapshot).get().getSnapshots(repoName)); + } + + public void testFindDanglingLatestGeneration() throws Exception { + Path repo = randomRepoPath(); + final String repoName = "test-repo"; + logger.info("--> creating repository at {}", repo.toAbsolutePath()); + assertAcked(client().admin().cluster().preparePutRepository(repoName) + .setType("fs").setSettings(Settings.builder() + .put("location", repo) + .put("compress", false) + .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); + + createIndex("test-idx-1", "test-idx-2"); + logger.info("--> indexing some data"); + indexRandom(true, + client().prepareIndex("test-idx-1").setSource("foo", "bar"), + client().prepareIndex("test-idx-2").setSource("foo", "bar")); + + final String snapshot = "test-snap"; + + logger.info("--> creating snapshot"); + CreateSnapshotResponse createSnapshotResponse = client().admin().cluster().prepareCreateSnapshot(repoName, snapshot) + .setWaitForCompletion(true).setIndices("test-idx-*").get(); + assertThat(createSnapshotResponse.getSnapshotInfo().successfulShards(), greaterThan(0)); + assertThat(createSnapshotResponse.getSnapshotInfo().successfulShards(), + equalTo(createSnapshotResponse.getSnapshotInfo().totalShards())); + + final Repository repository = internalCluster().getCurrentMasterNodeInstance(RepositoriesService.class).repository(repoName); + + logger.info("--> move index-N blob to next generation"); + final RepositoryData repositoryData = getRepositoryData(repository); + final long beforeMoveGen = repositoryData.getGenId(); + Files.move(repo.resolve("index-" + beforeMoveGen), repo.resolve("index-" + (beforeMoveGen + 1))); + + logger.info("--> set next generation as pending in the cluster state"); + final PlainActionFuture csUpdateFuture = PlainActionFuture.newFuture(); + internalCluster().getCurrentMasterNodeInstance(ClusterService.class).submitStateUpdateTask("set pending generation", + new ClusterStateUpdateTask() { + @Override + public ClusterState execute(ClusterState currentState) { + return ClusterState.builder(currentState).metaData(MetaData.builder(currentState.getMetaData()) + .putCustom(RepositoriesMetaData.TYPE, + currentState.metaData().custom(RepositoriesMetaData.TYPE).withUpdatedGeneration( + repository.getMetadata().name(), beforeMoveGen, beforeMoveGen + 1)).build()).build(); + } + + @Override + public void onFailure(String source, Exception e) { + csUpdateFuture.onFailure(e); + } + + @Override + public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { + csUpdateFuture.onResponse(null); + } + } + ); + csUpdateFuture.get(); + + logger.info("--> full cluster restart"); + internalCluster().fullRestart(); + ensureGreen(); + + Repository repositoryAfterRestart = internalCluster().getCurrentMasterNodeInstance(RepositoriesService.class).repository(repoName); + + logger.info("--> verify index-N blob is found at the new location"); + assertThat(getRepositoryData(repositoryAfterRestart).getGenId(), is(beforeMoveGen + 1)); + + logger.info("--> delete snapshot"); + client().admin().cluster().prepareDeleteSnapshot(repoName, snapshot).get(); + + logger.info("--> verify index-N blob is found at the expected location"); + assertThat(getRepositoryData(repositoryAfterRestart).getGenId(), is(beforeMoveGen + 2)); + + logger.info("--> make sure snapshot doesn't exist"); + expectThrows(SnapshotMissingException.class, () -> client().admin().cluster().prepareGetSnapshots(repoName) + .addSnapshots(snapshot).get().getSnapshots(repoName)); + } + + private void assertRepositoryBlocked(Client client, String repo, String existingSnapshot) { + logger.info("--> try to delete snapshot"); + final RepositoryException repositoryException3 = expectThrows(RepositoryException.class, + () -> client.admin().cluster().prepareDeleteSnapshot(repo, existingSnapshot).execute().actionGet()); + assertThat(repositoryException3.getMessage(), + containsString("Could not read repository data because the contents of the repository do not match its expected state.")); + + logger.info("--> try to create snapshot"); + final RepositoryException repositoryException4 = expectThrows(RepositoryException.class, + () -> client.admin().cluster().prepareCreateSnapshot(repo, existingSnapshot).execute().actionGet()); + assertThat(repositoryException4.getMessage(), + containsString("Could not read repository data because the contents of the repository do not match its expected state.")); + } +} diff --git a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java index 47e2626a3b7f3..7443eaded77a7 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/mockstore/MockEventuallyConsistentRepositoryTests.java @@ -140,6 +140,8 @@ public void testOverwriteSnapshotInfoBlob() { try (BlobStoreRepository repository = new MockEventuallyConsistentRepository(metaData, xContentRegistry(), clusterService, blobStoreContext, random())) { clusterService.addStateApplier(event -> repository.updateState(event.state())); + // Apply state once to initialize repo properly like RepositoriesService would + repository.updateState(clusterService.state()); repository.start(); // We create a snap- blob for snapshot "foo" in the first generation diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/AbstractThirdPartyRepositoryTestCase.java b/test/framework/src/main/java/org/elasticsearch/repositories/AbstractThirdPartyRepositoryTestCase.java index c6e76ac7174ed..efc0e653edf5c 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/AbstractThirdPartyRepositoryTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/AbstractThirdPartyRepositoryTestCase.java @@ -76,6 +76,7 @@ public void setUp() throws Exception { @Override public void tearDown() throws Exception { deleteAndAssertEmpty(getRepository().basePath()); + client().admin().cluster().prepareDeleteRepository("test-repo").get(); super.tearDown(); } @@ -169,8 +170,6 @@ protected void assertBlobsByPrefix(BlobPath path, String prefix, Map currentState = new AtomicReference<>(initialState); + // Setting local node as master so it may update the repository metadata in the cluster state + final DiscoveryNode localNode = new DiscoveryNode("", buildNewFakeTransportAddress(), Version.CURRENT); + final AtomicReference currentState = new AtomicReference<>( + ClusterState.builder(initialState).nodes( + DiscoveryNodes.builder().add(localNode).masterNodeId(localNode.getId()).localNodeId(localNode.getId()).build()).build()); when(clusterService.state()).then(invocationOnMock -> currentState.get()); final List appliers = new CopyOnWriteArrayList<>(); doAnswer(invocation -> { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java index dba66e0b1b1db..bf6512cc531e2 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java @@ -32,6 +32,7 @@ import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.routing.ShardRoutingState; import org.elasticsearch.cluster.routing.TestShardRouting; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.lucene.uid.Versions; @@ -352,8 +353,12 @@ private Environment createEnvironment() { private Repository createRepository() { Settings settings = Settings.builder().put("location", randomAlphaOfLength(10)).build(); RepositoryMetaData repositoryMetaData = new RepositoryMetaData(randomAlphaOfLength(10), FsRepository.TYPE, settings); - return new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), - BlobStoreTestUtil.mockClusterService(repositoryMetaData)); + final ClusterService clusterService = BlobStoreTestUtil.mockClusterService(repositoryMetaData); + final Repository repository = new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry(), clusterService); + clusterService.addStateApplier(e -> repository.updateState(e.state())); + // Apply state once to initialize repo properly like RepositoriesService would + repository.updateState(clusterService.state()); + return repository; } private static void runAsSnapshot(ThreadPool pool, Runnable runnable) { From f2045ab938bc0f79680978e511a6fa5b574bd5cc Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 17 Dec 2019 11:26:50 +0100 Subject: [PATCH 230/686] Fix ingest simulate response document order if processor executes async (#50244) If a processor executes asynchronously and the ingest simulate api simulates with multiple documents then the order of the documents in the response may not match the order of the documents in the request. Alexander Reelsen discovered this issue with the enrich processor with the following reproduction: ``` PUT cities/_doc/munich {"zip":"80331","city":"Munich"} PUT cities/_doc/berlin {"zip":"10965","city":"Berlin"} PUT /_enrich/policy/zip-policy { "match": { "indices": "cities", "match_field": "zip", "enrich_fields": [ "city" ] } } POST /_enrich/policy/zip-policy/_execute GET _cat/indices/.enrich-* POST /_ingest/pipeline/_simulate { "pipeline": { "processors" : [ { "enrich" : { "policy_name": "zip-policy", "field" : "zip", "target_field": "city", "max_matches": "1" } } ] }, "docs": [ { "_id": "first", "_source" : { "zip" : "80331" } } , { "_id": "second", "_source" : { "zip" : "50667" } } ] } ``` --- .../ingest/SimulateExecutionService.java | 8 ++- .../ingest/SimulateExecutionServiceTests.java | 62 +++++++++++++++++++ .../action/EnrichCoordinatorProxyAction.java | 5 +- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/ingest/SimulateExecutionService.java b/server/src/main/java/org/elasticsearch/action/ingest/SimulateExecutionService.java index 79de0d0c2a7fd..09bba6eb0da07 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/SimulateExecutionService.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/SimulateExecutionService.java @@ -67,17 +67,21 @@ void executeDocument(Pipeline pipeline, IngestDocument ingestDocument, boolean v public void execute(SimulatePipelineRequest.Parsed request, ActionListener listener) { threadPool.executor(THREAD_POOL_NAME).execute(ActionRunnable.wrap(listener, l -> { final AtomicInteger counter = new AtomicInteger(); - final List responses = new CopyOnWriteArrayList<>(); + final List responses = + new CopyOnWriteArrayList<>(new SimulateDocumentBaseResult[request.getDocuments().size()]); + int iter = 0; for (IngestDocument ingestDocument : request.getDocuments()) { + final int index = iter; executeDocument(request.getPipeline(), ingestDocument, request.isVerbose(), (response, e) -> { if (response != null) { - responses.add(response); + responses.set(index, response); } if (counter.incrementAndGet() == request.getDocuments().size()) { l.onResponse(new SimulatePipelineResponse(request.getPipeline().getId(), request.isVerbose(), responses)); } }); + iter++; } })); } diff --git a/server/src/test/java/org/elasticsearch/action/ingest/SimulateExecutionServiceTests.java b/server/src/test/java/org/elasticsearch/action/ingest/SimulateExecutionServiceTests.java index 2ced9d1e23dd2..7276ef2ebada4 100644 --- a/server/src/test/java/org/elasticsearch/action/ingest/SimulateExecutionServiceTests.java +++ b/server/src/test/java/org/elasticsearch/action/ingest/SimulateExecutionServiceTests.java @@ -19,6 +19,9 @@ package org.elasticsearch.action.ingest; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.index.VersionType; +import org.elasticsearch.ingest.AbstractProcessor; import org.elasticsearch.ingest.CompoundProcessor; import org.elasticsearch.ingest.DropProcessor; import org.elasticsearch.ingest.IngestDocument; @@ -29,17 +32,24 @@ import org.elasticsearch.ingest.TestProcessor; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; import org.junit.After; import org.junit.Before; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import static org.elasticsearch.ingest.IngestDocumentMatcher.assertIngestDocument; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -330,4 +340,56 @@ public void testDropDocumentVerboseExtraProcessor() throws Exception { assertThat(verboseResult.getProcessorResults().get(1).getFailure(), nullValue()); } + public void testAsyncSimulation() throws Exception { + int numDocs = randomIntBetween(1, 64); + List documents = new ArrayList<>(numDocs); + for (int id = 0; id < numDocs; id++) { + documents.add(new IngestDocument("_index", Integer.toString(id), null, 0L, VersionType.INTERNAL, new HashMap<>())); + } + Processor processor1 = new AbstractProcessor(null) { + + @Override + public void execute(IngestDocument ingestDocument, BiConsumer handler) { + threadPool.executor(ThreadPool.Names.GENERIC).execute(() -> { + ingestDocument.setFieldValue("processed", true); + handler.accept(ingestDocument, null); + }); + } + + @Override + public IngestDocument execute(IngestDocument ingestDocument) throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + public String getType() { + return "none-of-your-business"; + } + }; + Pipeline pipeline = new Pipeline("_id", "_description", version, new CompoundProcessor(processor1)); + SimulatePipelineRequest.Parsed request = new SimulatePipelineRequest.Parsed(pipeline, documents, false); + + AtomicReference responseHolder = new AtomicReference<>(); + AtomicReference errorHolder = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + executionService.execute(request, ActionListener.wrap(response -> { + responseHolder.set(response); + latch.countDown(); + }, e -> { + errorHolder.set(e); + latch.countDown(); + })); + latch.await(1, TimeUnit.MINUTES); + assertThat(errorHolder.get(), nullValue()); + SimulatePipelineResponse response = responseHolder.get(); + assertThat(response, notNullValue()); + assertThat(response.getResults().size(), equalTo(numDocs)); + + for (int id = 0; id < numDocs; id++) { + SimulateDocumentBaseResult result = (SimulateDocumentBaseResult) response.getResults().get(id); + assertThat(result.getIngestDocument().getMetadata().get(IngestDocument.MetaData.ID), equalTo(Integer.toString(id))); + assertThat(result.getIngestDocument().getSourceAndMetadata().get("processed"), is(true)); + } + } + } diff --git a/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/action/EnrichCoordinatorProxyAction.java b/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/action/EnrichCoordinatorProxyAction.java index add1a90c21a60..e285c8fef233e 100644 --- a/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/action/EnrichCoordinatorProxyAction.java +++ b/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/action/EnrichCoordinatorProxyAction.java @@ -63,7 +63,10 @@ public TransportAction(TransportService transportService, ActionFilters actionFi @Override protected void doExecute(Task task, SearchRequest request, ActionListener listener) { - assert Thread.currentThread().getName().contains(ThreadPool.Names.WRITE); + // Write tp is expected when executing enrich processor from index / bulk api + // Management tp is expected when executing enrich processor from ingest simulate api + assert Thread.currentThread().getName().contains(ThreadPool.Names.WRITE) + || Thread.currentThread().getName().contains(ThreadPool.Names.MANAGEMENT); coordinator.schedule(request, listener); } } From fe0850738d2e1b46c3abd46df5638dc8a0bdc1d2 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Tue, 17 Dec 2019 13:51:51 +0100 Subject: [PATCH 231/686] Simplify InternalTestCluster.fullRestart (#50218) With node ordinals gone, there's no longer a need for such a complicated full cluster restart procedure (as we can now uniquely associate nodes to data folders). Follow-up to #41652 --- .../test/InternalTestCluster.java | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java index 9b609040379eb..fe9cc9f8449f6 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java @@ -53,6 +53,7 @@ import org.elasticsearch.cluster.routing.allocation.decider.ThrottlingAllocationDecider; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Randomness; import org.elasticsearch.common.Strings; import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.common.component.LifecycleListener; @@ -1729,50 +1730,29 @@ private void removeExclusions(Set excludedNodeIds) { public synchronized void fullRestart(RestartCallback callback) throws Exception { int numNodesRestarted = 0; final Settings[] newNodeSettings = new Settings[nextNodeId.get()]; - Map, List> nodesByRoles = new HashMap<>(); - Set[] rolesOrderedByOriginalStartupOrder = new Set[nextNodeId.get()]; - final int nodeCount = nodes.size(); + final List toStartAndPublish = new ArrayList<>(); // we want to start nodes in one go for (NodeAndClient nodeAndClient : nodes.values()) { callback.doAfterNodes(numNodesRestarted++, nodeAndClient.nodeClient()); logger.info("Stopping and resetting node [{}] ", nodeAndClient.name); if (activeDisruptionScheme != null) { activeDisruptionScheme.removeFromNode(nodeAndClient.name, this); } - DiscoveryNode discoveryNode = getInstanceFromNode(ClusterService.class, nodeAndClient.node()).localNode(); final Settings newSettings = nodeAndClient.closeForRestart(callback); newNodeSettings[nodeAndClient.nodeAndClientId()] = newSettings; - rolesOrderedByOriginalStartupOrder[nodeAndClient.nodeAndClientId()] = discoveryNode.getRoles(); - nodesByRoles.computeIfAbsent(discoveryNode.getRoles(), k -> new ArrayList<>()).add(nodeAndClient); + toStartAndPublish.add(nodeAndClient); } callback.onAllNodesStopped(); - assert nodesByRoles.values().stream().mapToInt(List::size).sum() == nodeCount; + // randomize start up order + Randomness.shuffle(toStartAndPublish); - // randomize start up order, but making sure that: - // 1) A data folder that was assigned to a data node will stay so - // 2) Data nodes will get the same node lock ordinal range, so custom index paths (where the ordinal is used) - // will still belong to data nodes - for (List sameRoleNodes : nodesByRoles.values()) { - Collections.shuffle(sameRoleNodes, random); - } - final List startUpOrder = new ArrayList<>(); - for (Set roles : rolesOrderedByOriginalStartupOrder) { - if (roles == null) { - // if some nodes were stopped, we want have a role for that ordinal - continue; - } - final List nodesByRole = nodesByRoles.get(roles); - startUpOrder.add(nodesByRole.remove(0)); + for (NodeAndClient nodeAndClient : toStartAndPublish) { + logger.info("recreating node [{}] ", nodeAndClient.name); + nodeAndClient.recreateNode(newNodeSettings[nodeAndClient.nodeAndClientId()], () -> rebuildUnicastHostFiles(toStartAndPublish)); } - assert nodesByRoles.values().stream().mapToInt(List::size).sum() == 0; - for (NodeAndClient nodeAndClient : startUpOrder) { - logger.info("creating node [{}] ", nodeAndClient.name); - nodeAndClient.recreateNode(newNodeSettings[nodeAndClient.nodeAndClientId()], () -> rebuildUnicastHostFiles(startUpOrder)); - } - - startAndPublishNodesAndClients(startUpOrder); + startAndPublishNodesAndClients(toStartAndPublish); if (callback.validateClusterForming()) { validateClusterFormed(); From 3fd83c1ef5fe9379031f8f5b4d5a6ff4acff1941 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Tue, 17 Dec 2019 13:52:39 +0100 Subject: [PATCH 232/686] Omit loading IndexMetaData when inspecting shards (#50214) Loading shard state information during shard allocation sometimes runs into a situation where a data node does not know yet how to look up the shard on disk if custom data paths are used. The current implementation loads the index metadata from disk to determine what the custom data path looks like. This PR removes this dependency, simplifying the lookup. Relates #48701 --- .../TransportIndicesShardStoresAction.java | 31 ++--- .../elasticsearch/env/NodeEnvironment.java | 39 +++---- .../gateway/AsyncShardFetch.java | 12 +- .../gateway/GatewayAllocator.java | 24 ++-- ...ransportNodesListGatewayStartedShards.java | 96 +++++++++++----- .../org/elasticsearch/index/IndexService.java | 4 +- .../elasticsearch/index/IndexSettings.java | 7 +- .../RemoveCorruptedShardDataCommand.java | 2 +- .../elasticsearch/index/shard/ShardPath.java | 20 ++-- .../elasticsearch/indices/IndicesService.java | 2 +- .../TransportNodesListShardStoreMetaData.java | 107 +++++++++++++----- .../env/NodeEnvironmentTests.java | 19 +--- .../gateway/AsyncShardFetchTests.java | 2 +- .../gateway/RecoveryFromGatewayIT.java | 6 +- .../index/shard/ShardPathTests.java | 33 ++---- .../indices/IndicesServiceTests.java | 6 +- 16 files changed, 246 insertions(+), 164 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/shards/TransportIndicesShardStoresAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/shards/TransportIndicesShardStoresAction.java index 6efb9c8e89bed..f692fb6046850 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/shards/TransportIndicesShardStoresAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/shards/TransportIndicesShardStoresAction.java @@ -32,6 +32,7 @@ import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.health.ClusterShardHealth; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; @@ -43,6 +44,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.collect.ImmutableOpenIntMap; import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.util.concurrent.CountDown; @@ -100,7 +102,7 @@ protected void masterOperation(Task task, IndicesShardStoresRequest request, Clu final RoutingTable routingTables = state.routingTable(); final RoutingNodes routingNodes = state.getRoutingNodes(); final String[] concreteIndices = indexNameExpressionResolver.concreteIndexNames(state, request); - final Set shardIdsToFetch = new HashSet<>(); + final Set> shardsToFetch = new HashSet<>(); logger.trace("using cluster state version [{}] to determine shards", state.version()); // collect relevant shard ids of the requested indices for fetching store infos @@ -109,11 +111,12 @@ protected void masterOperation(Task task, IndicesShardStoresRequest request, Clu if (indexShardRoutingTables == null) { continue; } + final String customDataPath = IndexMetaData.INDEX_DATA_PATH_SETTING.get(state.metaData().index(index).getSettings()); for (IndexShardRoutingTable routing : indexShardRoutingTables) { final int shardId = routing.shardId().id(); ClusterShardHealth shardHealth = new ClusterShardHealth(shardId, routing); if (request.shardStatuses().contains(shardHealth.getStatus())) { - shardIdsToFetch.add(routing.shardId()); + shardsToFetch.add(Tuple.tuple(routing.shardId(), customDataPath)); } } } @@ -123,7 +126,7 @@ protected void masterOperation(Task task, IndicesShardStoresRequest request, Clu // we could fetch all shard store info from every node once (nNodes requests) // we have to implement a TransportNodesAction instead of using TransportNodesListGatewayStartedShards // for fetching shard stores info, that operates on a list of shards instead of a single shard - new AsyncShardStoresInfoFetches(state.nodes(), routingNodes, shardIdsToFetch, listener).start(); + new AsyncShardStoresInfoFetches(state.nodes(), routingNodes, shardsToFetch, listener).start(); } @Override @@ -135,46 +138,46 @@ protected ClusterBlockException checkBlock(IndicesShardStoresRequest request, Cl private class AsyncShardStoresInfoFetches { private final DiscoveryNodes nodes; private final RoutingNodes routingNodes; - private final Set shardIds; + private final Set> shards; private final ActionListener listener; private CountDown expectedOps; private final Queue fetchResponses; - AsyncShardStoresInfoFetches(DiscoveryNodes nodes, RoutingNodes routingNodes, Set shardIds, + AsyncShardStoresInfoFetches(DiscoveryNodes nodes, RoutingNodes routingNodes, Set> shards, ActionListener listener) { this.nodes = nodes; this.routingNodes = routingNodes; - this.shardIds = shardIds; + this.shards = shards; this.listener = listener; this.fetchResponses = new ConcurrentLinkedQueue<>(); - this.expectedOps = new CountDown(shardIds.size()); + this.expectedOps = new CountDown(shards.size()); } void start() { - if (shardIds.isEmpty()) { + if (shards.isEmpty()) { listener.onResponse(new IndicesShardStoresResponse()); } else { // explicitely type lister, some IDEs (Eclipse) are not able to correctly infer the function type Lister, NodeGatewayStartedShards> lister = this::listStartedShards; - for (ShardId shardId : shardIds) { - InternalAsyncFetch fetch = new InternalAsyncFetch(logger, "shard_stores", shardId, lister); + for (Tuple shard : shards) { + InternalAsyncFetch fetch = new InternalAsyncFetch(logger, "shard_stores", shard.v1(), shard.v2(), lister); fetch.fetchData(nodes, Collections.emptySet()); } } } - private void listStartedShards(ShardId shardId, DiscoveryNode[] nodes, + private void listStartedShards(ShardId shardId, String customDataPath, DiscoveryNode[] nodes, ActionListener> listener) { - var request = new TransportNodesListGatewayStartedShards.Request(shardId, nodes); + var request = new TransportNodesListGatewayStartedShards.Request(shardId, customDataPath, nodes); client.executeLocally(TransportNodesListGatewayStartedShards.TYPE, request, ActionListener.wrap(listener::onResponse, listener::onFailure)); } private class InternalAsyncFetch extends AsyncShardFetch { - InternalAsyncFetch(Logger logger, String type, ShardId shardId, + InternalAsyncFetch(Logger logger, String type, ShardId shardId, String customDataPath, Lister, NodeGatewayStartedShards> action) { - super(logger, type, shardId, action); + super(logger, type, shardId, customDataPath, action); } @Override diff --git a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java index 160662a63e5b3..ec5b4fe43c8fe 100644 --- a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java +++ b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java @@ -22,6 +22,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.logging.log4j.util.Strings; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.SegmentInfos; import org.apache.lucene.store.Directory; @@ -610,7 +611,7 @@ public void deleteShardDirectoryUnderLock(ShardLock lock, IndexSettings indexSet acquireFSLockForPaths(indexSettings, paths); IOUtils.rm(paths); if (indexSettings.hasCustomDataPath()) { - Path customLocation = resolveCustomLocation(indexSettings, shardId); + Path customLocation = resolveCustomLocation(indexSettings.customDataPath(), shardId); logger.trace("acquiring lock for {}, custom path: [{}]", shardId, customLocation); acquireFSLockForPaths(indexSettings, customLocation); logger.trace("deleting custom shard {} directory [{}]", shardId, customLocation); @@ -687,7 +688,7 @@ public void deleteIndexDirectoryUnderLock(Index index, IndexSettings indexSettin logger.trace("deleting index {} directory, paths({}): [{}]", index, indexPaths.length, indexPaths); IOUtils.rm(indexPaths); if (indexSettings.hasCustomDataPath()) { - Path customLocation = resolveIndexCustomLocation(indexSettings); + Path customLocation = resolveIndexCustomLocation(indexSettings.customDataPath(), index.getUUID()); logger.trace("deleting custom index {} directory [{}]", index, customLocation); IOUtils.rm(customLocation); } @@ -933,7 +934,7 @@ public Path[] indexPaths(Index index) { * returned paths. The returned array may contain paths to non-existing directories. * * @see IndexSettings#hasCustomDataPath() - * @see #resolveCustomLocation(IndexSettings, ShardId) + * @see #resolveCustomLocation(String, ShardId) * */ public Path[] availableShardPaths(ShardId shardId) { @@ -1233,17 +1234,12 @@ private static boolean isIndexMetaDataPath(Path path) { /** * Resolve the custom path for a index's shard. - * Uses the {@code IndexMetaData.SETTING_DATA_PATH} setting to determine - * the root path for the index. - * - * @param indexSettings settings for the index */ - public static Path resolveBaseCustomLocation(IndexSettings indexSettings, Path sharedDataPath) { - String customDataDir = indexSettings.customDataPath(); - if (customDataDir != null) { + public static Path resolveBaseCustomLocation(String customDataPath, Path sharedDataPath) { + if (Strings.isNotEmpty(customDataPath)) { // This assert is because this should be caught by MetaDataCreateIndexService assert sharedDataPath != null; - return sharedDataPath.resolve(customDataDir).resolve("0"); + return sharedDataPath.resolve(customDataPath).resolve("0"); } else { throw new IllegalArgumentException("no custom " + IndexMetaData.SETTING_DATA_PATH + " setting available"); } @@ -1254,14 +1250,14 @@ public static Path resolveBaseCustomLocation(IndexSettings indexSettings, Path s * Uses the {@code IndexMetaData.SETTING_DATA_PATH} setting to determine * the root path for the index. * - * @param indexSettings settings for the index + * @param customDataPath the custom data path */ - private Path resolveIndexCustomLocation(IndexSettings indexSettings) { - return resolveIndexCustomLocation(indexSettings, sharedDataPath); + private Path resolveIndexCustomLocation(String customDataPath, String indexUUID) { + return resolveIndexCustomLocation(customDataPath, indexUUID, sharedDataPath); } - private static Path resolveIndexCustomLocation(IndexSettings indexSettings, Path sharedDataPath) { - return resolveBaseCustomLocation(indexSettings, sharedDataPath).resolve(indexSettings.getUUID()); + private static Path resolveIndexCustomLocation(String customDataPath, String indexUUID, Path sharedDataPath) { + return resolveBaseCustomLocation(customDataPath, sharedDataPath).resolve(indexUUID); } /** @@ -1269,15 +1265,16 @@ private static Path resolveIndexCustomLocation(IndexSettings indexSettings, Path * Uses the {@code IndexMetaData.SETTING_DATA_PATH} setting to determine * the root path for the index. * - * @param indexSettings settings for the index + * @param customDataPath the custom data path * @param shardId shard to resolve the path to */ - public Path resolveCustomLocation(IndexSettings indexSettings, final ShardId shardId) { - return resolveCustomLocation(indexSettings, shardId, sharedDataPath); + public Path resolveCustomLocation(String customDataPath, final ShardId shardId) { + return resolveCustomLocation(customDataPath, shardId, sharedDataPath); } - public static Path resolveCustomLocation(IndexSettings indexSettings, final ShardId shardId, Path sharedDataPath) { - return resolveIndexCustomLocation(indexSettings, sharedDataPath).resolve(Integer.toString(shardId.id())); + public static Path resolveCustomLocation(String customDataPath, final ShardId shardId, Path sharedDataPath) { + return resolveIndexCustomLocation(customDataPath, shardId.getIndex().getUUID(), + sharedDataPath).resolve(Integer.toString(shardId.id())); } /** diff --git a/server/src/main/java/org/elasticsearch/gateway/AsyncShardFetch.java b/server/src/main/java/org/elasticsearch/gateway/AsyncShardFetch.java index 007357ee54a75..43f7a29cbcb95 100644 --- a/server/src/main/java/org/elasticsearch/gateway/AsyncShardFetch.java +++ b/server/src/main/java/org/elasticsearch/gateway/AsyncShardFetch.java @@ -42,6 +42,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; @@ -61,22 +62,25 @@ public abstract class AsyncShardFetch implements Rel * An action that lists the relevant shard data that needs to be fetched. */ public interface Lister, NodeResponse extends BaseNodeResponse> { - void list(ShardId shardId, DiscoveryNode[] nodes, ActionListener listener); + void list(ShardId shardId, @Nullable String customDataPath, DiscoveryNode[] nodes, ActionListener listener); } protected final Logger logger; protected final String type; protected final ShardId shardId; + protected final String customDataPath; private final Lister, T> action; private final Map> cache = new HashMap<>(); private final Set nodesToIgnore = new HashSet<>(); private final AtomicLong round = new AtomicLong(); private boolean closed; - protected AsyncShardFetch(Logger logger, String type, ShardId shardId, Lister, T> action) { + protected AsyncShardFetch(Logger logger, String type, ShardId shardId, String customDataPath, + Lister, T> action) { this.logger = logger; this.type = type; - this.shardId = shardId; + this.shardId = Objects.requireNonNull(shardId); + this.customDataPath = Objects.requireNonNull(customDataPath); this.action = (Lister, T>) action; } @@ -285,7 +289,7 @@ private boolean hasAnyNodeFetching(Map> shardCache) { // visible for testing void asyncFetch(final DiscoveryNode[] nodes, long fetchingRound) { logger.trace("{} fetching [{}] from {}", shardId, type, nodes); - action.list(shardId, nodes, new ActionListener>() { + action.list(shardId, customDataPath, nodes, new ActionListener>() { @Override public void onResponse(BaseNodesResponse response) { processAsyncFetch(response.getNodes(), response.failures(), fetchingRound); diff --git a/server/src/main/java/org/elasticsearch/gateway/GatewayAllocator.java b/server/src/main/java/org/elasticsearch/gateway/GatewayAllocator.java index a83e3b4cfd782..a1319bcd59031 100644 --- a/server/src/main/java/org/elasticsearch/gateway/GatewayAllocator.java +++ b/server/src/main/java/org/elasticsearch/gateway/GatewayAllocator.java @@ -27,6 +27,7 @@ import org.elasticsearch.action.support.nodes.BaseNodeResponse; import org.elasticsearch.action.support.nodes.BaseNodesResponse; import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.routing.RerouteService; @@ -189,8 +190,9 @@ private boolean hasNewNodes(DiscoveryNodes nodes) { class InternalAsyncFetch extends AsyncShardFetch { - InternalAsyncFetch(Logger logger, String type, ShardId shardId, Lister, T> action) { - super(logger, type, shardId, action); + InternalAsyncFetch(Logger logger, String type, ShardId shardId, String customDataPath, + Lister, T> action) { + super(logger, type, shardId, customDataPath, action); } @Override @@ -217,7 +219,9 @@ protected AsyncShardFetch.FetchResult fetchData(ShardR Lister, NodeGatewayStartedShards> lister = this::listStartedShards; AsyncShardFetch fetch = asyncFetchStarted.computeIfAbsent(shard.shardId(), - shardId -> new InternalAsyncFetch<>(logger, "shard_started", shardId, lister)); + shardId -> new InternalAsyncFetch<>(logger, "shard_started", shardId, + IndexMetaData.INDEX_DATA_PATH_SETTING.get(allocation.metaData().index(shard.index()).getSettings()), + lister)); AsyncShardFetch.FetchResult shardState = fetch.fetchData(allocation.nodes(), allocation.getIgnoreNodes(shard.shardId())); @@ -227,9 +231,9 @@ protected AsyncShardFetch.FetchResult fetchData(ShardR return shardState; } - private void listStartedShards(ShardId shardId, DiscoveryNode[] nodes, + private void listStartedShards(ShardId shardId, String customDataPath, DiscoveryNode[] nodes, ActionListener> listener) { - var request = new TransportNodesListGatewayStartedShards.Request(shardId, nodes); + var request = new TransportNodesListGatewayStartedShards.Request(shardId, customDataPath, nodes); client.executeLocally(TransportNodesListGatewayStartedShards.TYPE, request, ActionListener.wrap(listener::onResponse, listener::onFailure)); } @@ -244,12 +248,12 @@ class InternalReplicaShardAllocator extends ReplicaShardAllocator { } @Override - protected AsyncShardFetch.FetchResult - fetchData(ShardRouting shard, RoutingAllocation allocation) { + protected AsyncShardFetch.FetchResult fetchData(ShardRouting shard, RoutingAllocation allocation) { // explicitely type lister, some IDEs (Eclipse) are not able to correctly infer the function type Lister, NodeStoreFilesMetaData> lister = this::listStoreFilesMetaData; AsyncShardFetch fetch = asyncFetchStore.computeIfAbsent(shard.shardId(), - shardId -> new InternalAsyncFetch<>(logger, "shard_store", shard.shardId(), lister)); + shardId -> new InternalAsyncFetch<>(logger, "shard_store", shard.shardId(), + IndexMetaData.INDEX_DATA_PATH_SETTING.get(allocation.metaData().index(shard.index()).getSettings()), lister)); AsyncShardFetch.FetchResult shardStores = fetch.fetchData(allocation.nodes(), allocation.getIgnoreNodes(shard.shardId())); if (shardStores.hasData()) { @@ -258,9 +262,9 @@ class InternalReplicaShardAllocator extends ReplicaShardAllocator { return shardStores; } - private void listStoreFilesMetaData(ShardId shardId, DiscoveryNode[] nodes, + private void listStoreFilesMetaData(ShardId shardId, String customDataPath, DiscoveryNode[] nodes, ActionListener> listener) { - var request = new TransportNodesListShardStoreMetaData.Request(shardId, nodes); + var request = new TransportNodesListShardStoreMetaData.Request(shardId, customDataPath, nodes); client.executeLocally(TransportNodesListShardStoreMetaData.TYPE, request, ActionListener.wrap(listener::onResponse, listener::onFailure)); } diff --git a/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java b/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java index ca68dfc9c1ccc..a99ba6ab2b323 100644 --- a/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java +++ b/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java @@ -21,6 +21,7 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionType; import org.elasticsearch.action.FailedNodeException; import org.elasticsearch.action.support.ActionFilters; @@ -33,6 +34,7 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -109,26 +111,24 @@ protected NodeGatewayStartedShards nodeOperation(NodeRequest request, Task task) ShardStateMetaData shardStateMetaData = ShardStateMetaData.FORMAT.loadLatestState(logger, namedXContentRegistry, nodeEnv.availableShardPaths(request.shardId)); if (shardStateMetaData != null) { - IndexMetaData metaData = clusterService.state().metaData().index(shardId.getIndex()); - if (metaData == null) { - // we may send this requests while processing the cluster state that recovered the index - // sometimes the request comes in before the local node processed that cluster state - // in such cases we can load it from disk - metaData = IndexMetaData.FORMAT.loadLatestState(logger, namedXContentRegistry, - nodeEnv.indexPaths(shardId.getIndex())); - } - if (metaData == null) { - ElasticsearchException e = new ElasticsearchException("failed to find local IndexMetaData"); - e.setShard(request.shardId); - throw e; - } - if (indicesService.getShardOrNull(shardId) == null) { + final String customDataPath; + if (request.getCustomDataPath() != null) { + customDataPath = request.getCustomDataPath(); + } else { + // TODO: Fallback for BWC with older ES versions. Remove once request.getCustomDataPath() always returns non-null + final IndexMetaData metaData = clusterService.state().metaData().index(shardId.getIndex()); + if (metaData != null) { + customDataPath = new IndexSettings(metaData, settings).customDataPath(); + } else { + logger.trace("{} node doesn't have meta data for the requests index", shardId); + throw new ElasticsearchException("node doesn't have meta data for index " + shardId.getIndex()); + } + } // we don't have an open shard on the store, validate the files on disk are openable ShardPath shardPath = null; try { - IndexSettings indexSettings = new IndexSettings(metaData, settings); - shardPath = ShardPath.loadShardPath(logger, nodeEnv, shardId, indexSettings); + shardPath = ShardPath.loadShardPath(logger, nodeEnv, shardId, customDataPath); if (shardPath == null) { throw new IllegalStateException(shardId + " no shard path found"); } @@ -162,27 +162,47 @@ protected NodeGatewayStartedShards nodeOperation(NodeRequest request, Task task) public static class Request extends BaseNodesRequest { - private ShardId shardId; + private final ShardId shardId; + @Nullable + private final String customDataPath; public Request(StreamInput in) throws IOException { super(in); shardId = new ShardId(in); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + customDataPath = in.readString(); + } else { + customDataPath = null; + } } - public Request(ShardId shardId, DiscoveryNode[] nodes) { + public Request(ShardId shardId, String customDataPath, DiscoveryNode[] nodes) { super(nodes); - this.shardId = shardId; + this.shardId = Objects.requireNonNull(shardId); + this.customDataPath = Objects.requireNonNull(customDataPath); } - public ShardId shardId() { - return this.shardId; + return shardId; + } + + /** + * Returns the custom data path that is used to look up information for this shard. + * Returns an empty string if no custom data path is used for this index. + * Returns null if custom data path information is not available (due to BWC). + */ + @Nullable + public String getCustomDataPath() { + return customDataPath; } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); shardId.writeTo(out); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeString(customDataPath); + } } } @@ -211,33 +231,55 @@ protected void writeNodesTo(StreamOutput out, List nod public static class NodeRequest extends BaseNodeRequest { - private ShardId shardId; + private final ShardId shardId; + @Nullable + private final String customDataPath; public NodeRequest(StreamInput in) throws IOException { super(in); shardId = new ShardId(in); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + customDataPath = in.readString(); + } else { + customDataPath = null; + } } public NodeRequest(Request request) { - this.shardId = request.shardId(); + this.shardId = Objects.requireNonNull(request.shardId()); + this.customDataPath = Objects.requireNonNull(request.getCustomDataPath()); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); shardId.writeTo(out); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + assert customDataPath != null; + out.writeString(customDataPath); + } } public ShardId getShardId() { return shardId; } + + /** + * Returns the custom data path that is used to look up information for this shard. + * Returns an empty string if no custom data path is used for this index. + * Returns null if custom data path information is not available (due to BWC). + */ + @Nullable + public String getCustomDataPath() { + return customDataPath; + } } public static class NodeGatewayStartedShards extends BaseNodeResponse { - private String allocationId = null; - private boolean primary = false; - private Exception storeException = null; + private final String allocationId; + private final boolean primary; + private final Exception storeException; public NodeGatewayStartedShards(StreamInput in) throws IOException { super(in); @@ -245,6 +287,8 @@ public NodeGatewayStartedShards(StreamInput in) throws IOException { primary = in.readBoolean(); if (in.readBoolean()) { storeException = in.readException(); + } else { + storeException = null; } } diff --git a/server/src/main/java/org/elasticsearch/index/IndexService.java b/server/src/main/java/org/elasticsearch/index/IndexService.java index 47c3bfb35fee7..481ad3f5a7f6f 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexService.java +++ b/server/src/main/java/org/elasticsearch/index/IndexService.java @@ -368,12 +368,12 @@ public synchronized IndexShard createShard( eventListener.beforeIndexShardCreated(shardId, indexSettings); ShardPath path; try { - path = ShardPath.loadShardPath(logger, nodeEnv, shardId, this.indexSettings); + path = ShardPath.loadShardPath(logger, nodeEnv, shardId, this.indexSettings.customDataPath()); } catch (IllegalStateException ex) { logger.warn("{} failed to load shard path, trying to remove leftover", shardId); try { ShardPath.deleteLeftoverShardDirectory(logger, nodeEnv, lock, this.indexSettings); - path = ShardPath.loadShardPath(logger, nodeEnv, shardId, this.indexSettings); + path = ShardPath.loadShardPath(logger, nodeEnv, shardId, this.indexSettings.customDataPath()); } catch (Exception inner) { ex.addSuppressed(inner); throw ex; diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index 99076f812b96e..cce17b8f441e7 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -19,6 +19,7 @@ package org.elasticsearch.index; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.util.Strings; import org.apache.lucene.index.MergePolicy; import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetaData; @@ -631,14 +632,14 @@ public String getUUID() { * Returns true if the index has a custom data path */ public boolean hasCustomDataPath() { - return customDataPath() != null; + return Strings.isNotEmpty(customDataPath()); } /** - * Returns the customDataPath for this index, if configured. null o.w. + * Returns the customDataPath for this index, if configured. "" o.w. */ public String customDataPath() { - return settings.get(IndexMetaData.SETTING_DATA_PATH); + return IndexMetaData.INDEX_DATA_PATH_SETTING.get(settings); } /** diff --git a/server/src/main/java/org/elasticsearch/index/shard/RemoveCorruptedShardDataCommand.java b/server/src/main/java/org/elasticsearch/index/shard/RemoveCorruptedShardDataCommand.java index 674124637113f..b7a178972fa72 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/RemoveCorruptedShardDataCommand.java +++ b/server/src/main/java/org/elasticsearch/index/shard/RemoveCorruptedShardDataCommand.java @@ -192,7 +192,7 @@ protected void findAndProcessShardPath(OptionSet options, Environment environmen if (Files.exists(shardPathLocation) == false) { continue; } - final ShardPath shardPath = ShardPath.loadShardPath(logger, shId, indexSettings, + final ShardPath shardPath = ShardPath.loadShardPath(logger, shId, indexSettings.customDataPath(), new Path[]{shardPathLocation}, nodePath.path); if (shardPath != null) { consumer.accept(shardPath); diff --git a/server/src/main/java/org/elasticsearch/index/shard/ShardPath.java b/server/src/main/java/org/elasticsearch/index/shard/ShardPath.java index ac865704d51bb..aab86d64ba2b6 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/ShardPath.java +++ b/server/src/main/java/org/elasticsearch/index/shard/ShardPath.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.util.Strings; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -114,13 +115,13 @@ public boolean isCustomDataPath() { /** * This method walks through the nodes shard paths to find the data and state path for the given shard. If multiple * directories with a valid shard state exist the one with the highest version will be used. - * Note: this method resolves custom data locations for the shard. + * Note: this method resolves custom data locations for the shard if such a custom data path is provided. */ public static ShardPath loadShardPath(Logger logger, NodeEnvironment env, - ShardId shardId, IndexSettings indexSettings) throws IOException { + ShardId shardId, String customDataPath) throws IOException { final Path[] paths = env.availableShardPaths(shardId); final Path sharedDataPath = env.sharedDataPath(); - return loadShardPath(logger, shardId, indexSettings, paths, sharedDataPath); + return loadShardPath(logger, shardId, customDataPath, paths, sharedDataPath); } /** @@ -128,9 +129,9 @@ public static ShardPath loadShardPath(Logger logger, NodeEnvironment env, * directories with a valid shard state exist the one with the highest version will be used. * Note: this method resolves custom data locations for the shard. */ - public static ShardPath loadShardPath(Logger logger, ShardId shardId, IndexSettings indexSettings, Path[] availableShardPaths, + public static ShardPath loadShardPath(Logger logger, ShardId shardId, String customDataPath, Path[] availableShardPaths, Path sharedDataPath) throws IOException { - final String indexUUID = indexSettings.getUUID(); + final String indexUUID = shardId.getIndex().getUUID(); Path loadedPath = null; for (Path path : availableShardPaths) { // EMPTY is safe here because we never call namedObject @@ -156,13 +157,14 @@ public static ShardPath loadShardPath(Logger logger, ShardId shardId, IndexSetti } else { final Path dataPath; final Path statePath = loadedPath; - if (indexSettings.hasCustomDataPath()) { - dataPath = NodeEnvironment.resolveCustomLocation(indexSettings, shardId, sharedDataPath); + final boolean hasCustomDataPath = Strings.isNotEmpty(customDataPath); + if (hasCustomDataPath) { + dataPath = NodeEnvironment.resolveCustomLocation(customDataPath, shardId, sharedDataPath); } else { dataPath = statePath; } logger.debug("{} loaded data path [{}], state path [{}]", shardId, dataPath, statePath); - return new ShardPath(indexSettings.hasCustomDataPath(), dataPath, statePath, shardId); + return new ShardPath(hasCustomDataPath, dataPath, statePath, shardId); } } @@ -195,7 +197,7 @@ public static ShardPath selectNewPathForShard(NodeEnvironment env, ShardId shard final Path statePath; if (indexSettings.hasCustomDataPath()) { - dataPath = env.resolveCustomLocation(indexSettings, shardId); + dataPath = env.resolveCustomLocation(indexSettings.customDataPath(), shardId); statePath = env.nodePaths()[0].resolve(shardId); } else { BigInteger totFreeSpace = BigInteger.ZERO; diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesService.java b/server/src/main/java/org/elasticsearch/indices/IndicesService.java index 4810f9f00e8e5..deba07bc76139 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesService.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesService.java @@ -971,7 +971,7 @@ public ShardDeletionCheckResult canDeleteShardContent(ShardId shardId, IndexSett } else if (indexSettings.hasCustomDataPath()) { // lets see if it's on a custom path (return false if the shared doesn't exist) // we don't need to delete anything that is not there - return Files.exists(nodeEnv.resolveCustomLocation(indexSettings, shardId)) ? + return Files.exists(nodeEnv.resolveCustomLocation(indexSettings.customDataPath(), shardId)) ? ShardDeletionCheckResult.FOLDER_FOUND_CAN_DELETE : ShardDeletionCheckResult.NO_FOLDER_FOUND; } else { diff --git a/server/src/main/java/org/elasticsearch/indices/store/TransportNodesListShardStoreMetaData.java b/server/src/main/java/org/elasticsearch/indices/store/TransportNodesListShardStoreMetaData.java index 97deeefa74da3..1e4ef6781a761 100644 --- a/server/src/main/java/org/elasticsearch/indices/store/TransportNodesListShardStoreMetaData.java +++ b/server/src/main/java/org/elasticsearch/indices/store/TransportNodesListShardStoreMetaData.java @@ -34,13 +34,13 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.IndexSettings; @@ -60,6 +60,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.concurrent.TimeUnit; public class TransportNodesListShardStoreMetaData extends TransportNodesAction { - private ShardId shardId; + private final ShardId shardId; + @Nullable + private final String customDataPath; public Request(StreamInput in) throws IOException { super(in); shardId = new ShardId(in); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + customDataPath = in.readString(); + } else { + customDataPath = null; + } } - public Request(ShardId shardId, DiscoveryNode[] nodes) { + public Request(ShardId shardId, String customDataPath, DiscoveryNode[] nodes) { super(nodes); - this.shardId = shardId; + this.shardId = Objects.requireNonNull(shardId); + this.customDataPath = Objects.requireNonNull(customDataPath); + } + + public ShardId shardId() { + return shardId; + } + + /** + * Returns the custom data path that is used to look up information for this shard. + * Returns an empty string if no custom data path is used for this index. + * Returns null if custom data path information is not available (due to BWC). + */ + @Nullable + public String getCustomDataPath() { + return customDataPath; } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); shardId.writeTo(out); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeString(customDataPath); + } } } @@ -302,21 +329,47 @@ protected void writeNodesTo(StreamOutput out, List nodes public static class NodeRequest extends BaseNodeRequest { - private ShardId shardId; + private final ShardId shardId; + @Nullable + private final String customDataPath; public NodeRequest(StreamInput in) throws IOException { super(in); shardId = new ShardId(in); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + customDataPath = in.readString(); + } else { + customDataPath = null; + } } - NodeRequest(TransportNodesListShardStoreMetaData.Request request) { - this.shardId = request.shardId; + public NodeRequest(Request request) { + this.shardId = Objects.requireNonNull(request.shardId()); + this.customDataPath = Objects.requireNonNull(request.getCustomDataPath()); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); shardId.writeTo(out); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + assert customDataPath != null; + out.writeString(customDataPath); + } + } + + public ShardId getShardId() { + return shardId; + } + + /** + * Returns the custom data path that is used to look up information for this shard. + * Returns an empty string if no custom data path is used for this index. + * Returns null if custom data path information is not available (due to BWC). + */ + @Nullable + public String getCustomDataPath() { + return customDataPath; } } diff --git a/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java b/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java index 5fddb2ee4c38d..19100343d2b15 100644 --- a/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java +++ b/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java @@ -19,14 +19,13 @@ package org.elasticsearch.env; import org.apache.lucene.index.SegmentInfos; -import org.elasticsearch.common.util.set.Sets; -import org.elasticsearch.core.internal.io.IOUtils; import org.apache.lucene.util.LuceneTestCase; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.AbstractRunnable; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.gateway.MetaDataStateFormat; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexSettings; @@ -357,19 +356,11 @@ public void testCustomDataPaths() throws Exception { String[] dataPaths = tmpPaths(); NodeEnvironment env = newNodeEnvironment(dataPaths, "/tmp", Settings.EMPTY); - final Settings indexSettings = Settings.builder().put(IndexMetaData.SETTING_INDEX_UUID, "myindexUUID").build(); - IndexSettings s1 = IndexSettingsModule.newIndexSettings("myindex", indexSettings); - IndexSettings s2 = IndexSettingsModule.newIndexSettings("myindex", Settings.builder() - .put(indexSettings) - .put(IndexMetaData.SETTING_DATA_PATH, "/tmp/foo").build()); Index index = new Index("myindex", "myindexUUID"); ShardId sid = new ShardId(index, 0); - assertFalse("no settings should mean no custom data path", s1.hasCustomDataPath()); - assertTrue("settings with path_data should have a custom data path", s2.hasCustomDataPath()); - assertThat(env.availableShardPaths(sid), equalTo(env.availableShardPaths(sid))); - assertThat(env.resolveCustomLocation(s2, sid).toAbsolutePath(), + assertThat(env.resolveCustomLocation("/tmp/foo", sid).toAbsolutePath(), equalTo(PathUtils.get("/tmp/foo/0/" + index.getUUID() + "/0").toAbsolutePath())); assertThat("shard paths with a custom data_path should contain only regular paths", @@ -379,10 +370,8 @@ public void testCustomDataPaths() throws Exception { assertThat("index paths uses the regular template", env.indexPaths(index), equalTo(stringsToPaths(dataPaths, "indices/" + index.getUUID()))); - IndexSettings s3 = new IndexSettings(s2.getIndexMetaData(), Settings.builder().build()); - assertThat(env.availableShardPaths(sid), equalTo(env.availableShardPaths(sid))); - assertThat(env.resolveCustomLocation(s3, sid).toAbsolutePath(), + assertThat(env.resolveCustomLocation("/tmp/foo", sid).toAbsolutePath(), equalTo(PathUtils.get("/tmp/foo/0/" + index.getUUID() + "/0").toAbsolutePath())); assertThat("shard paths with a custom data_path should contain only regular paths", diff --git a/server/src/test/java/org/elasticsearch/gateway/AsyncShardFetchTests.java b/server/src/test/java/org/elasticsearch/gateway/AsyncShardFetchTests.java index b4ce705735b7c..259c806bea06a 100644 --- a/server/src/test/java/org/elasticsearch/gateway/AsyncShardFetchTests.java +++ b/server/src/test/java/org/elasticsearch/gateway/AsyncShardFetchTests.java @@ -373,7 +373,7 @@ static class Entry { private AtomicInteger reroute = new AtomicInteger(); TestFetch(ThreadPool threadPool) { - super(LogManager.getLogger(TestFetch.class), "test", new ShardId("test", "_na_", 1), null); + super(LogManager.getLogger(TestFetch.class), "test", new ShardId("test", "_na_", 1), "", null); this.threadPool = threadPool; } diff --git a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java index 126c7eeb21ca7..e3a754f0003db 100644 --- a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java +++ b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java @@ -542,7 +542,9 @@ public void assertSyncIdsNotNull() { public void testStartedShardFoundIfStateNotYetProcessed() throws Exception { // nodes may need to report the shards they processed the initial recovered cluster state from the master final String nodeName = internalCluster().startNode(); - assertAcked(prepareCreate("test").setSettings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 1))); + createIndex("test", Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 1).build()); + final String customDataPath = IndexMetaData.INDEX_DATA_PATH_SETTING.get( + client().admin().indices().prepareGetSettings("test").get().getIndexToSettings().get("test")); final Index index = resolveIndex("test"); final ShardId shardId = new ShardId(index, 0); indexDoc("test", "1"); @@ -579,7 +581,7 @@ public Settings onNodeStopped(String nodeName) throws Exception { TransportNodesListGatewayStartedShards.NodesGatewayStartedShards response; response = ActionTestUtils.executeBlocking(internalCluster().getInstance(TransportNodesListGatewayStartedShards.class), - new TransportNodesListGatewayStartedShards.Request(shardId, new DiscoveryNode[]{node})); + new TransportNodesListGatewayStartedShards.Request(shardId, customDataPath, new DiscoveryNode[]{node})); assertThat(response.getNodes(), hasSize(1)); assertThat(response.getNodes().get(0).allocationId(), notNullValue()); diff --git a/server/src/test/java/org/elasticsearch/index/shard/ShardPathTests.java b/server/src/test/java/org/elasticsearch/index/shard/ShardPathTests.java index cbaae21476855..721e5435172a3 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/ShardPathTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/ShardPathTests.java @@ -18,15 +18,12 @@ */ package org.elasticsearch.index.shard; -import org.elasticsearch.Version; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.routing.AllocationId; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.Index; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.IndexSettingsModule; import java.io.IOException; import java.nio.file.Path; @@ -37,16 +34,12 @@ public class ShardPathTests extends ESTestCase { public void testLoadShardPath() throws IOException { try (NodeEnvironment env = newNodeEnvironment(Settings.builder().build())) { - Settings.Builder builder = Settings.builder().put(IndexMetaData.SETTING_INDEX_UUID, "0xDEADBEEF") - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT); - Settings settings = builder.build(); ShardId shardId = new ShardId("foo", "0xDEADBEEF", 0); Path[] paths = env.availableShardPaths(shardId); Path path = randomFrom(paths); ShardStateMetaData.FORMAT.writeAndCleanup( new ShardStateMetaData(true, "0xDEADBEEF", AllocationId.newInitializing()), path); - ShardPath shardPath = - ShardPath.loadShardPath(logger, env, shardId, IndexSettingsModule.newIndexSettings(shardId.getIndex(), settings)); + ShardPath shardPath = ShardPath.loadShardPath(logger, env, shardId, ""); assertEquals(path, shardPath.getDataPath()); assertEquals("0xDEADBEEF", shardPath.getShardId().getIndex().getUUID()); assertEquals("foo", shardPath.getShardId().getIndexName()); @@ -58,32 +51,24 @@ public void testLoadShardPath() throws IOException { public void testFailLoadShardPathOnMultiState() throws IOException { try (NodeEnvironment env = newNodeEnvironment(Settings.builder().build())) { final String indexUUID = "0xDEADBEEF"; - Settings.Builder builder = Settings.builder().put(IndexMetaData.SETTING_INDEX_UUID, indexUUID) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT); - Settings settings = builder.build(); ShardId shardId = new ShardId("foo", indexUUID, 0); Path[] paths = env.availableShardPaths(shardId); assumeTrue("This test tests multi data.path but we only got one", paths.length > 1); ShardStateMetaData.FORMAT.writeAndCleanup( new ShardStateMetaData(true, indexUUID, AllocationId.newInitializing()), paths); - Exception e = expectThrows(IllegalStateException.class, () -> - ShardPath.loadShardPath(logger, env, shardId, IndexSettingsModule.newIndexSettings(shardId.getIndex(), settings))); + Exception e = expectThrows(IllegalStateException.class, () -> ShardPath.loadShardPath(logger, env, shardId, "")); assertThat(e.getMessage(), containsString("more than one shard state found")); } } public void testFailLoadShardPathIndexUUIDMissmatch() throws IOException { try (NodeEnvironment env = newNodeEnvironment(Settings.builder().build())) { - Settings.Builder builder = Settings.builder().put(IndexMetaData.SETTING_INDEX_UUID, "foobar") - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT); - Settings settings = builder.build(); ShardId shardId = new ShardId("foo", "foobar", 0); Path[] paths = env.availableShardPaths(shardId); Path path = randomFrom(paths); ShardStateMetaData.FORMAT.writeAndCleanup( new ShardStateMetaData(true, "0xDEADBEEF", AllocationId.newInitializing()), path); - Exception e = expectThrows(IllegalStateException.class, () -> - ShardPath.loadShardPath(logger, env, shardId, IndexSettingsModule.newIndexSettings(shardId.getIndex(), settings))); + Exception e = expectThrows(IllegalStateException.class, () -> ShardPath.loadShardPath(logger, env, shardId, "")); assertThat(e.getMessage(), containsString("expected: foobar on shard path")); } } @@ -107,22 +92,19 @@ public void testValidCtor() { public void testGetRootPaths() throws IOException { boolean useCustomDataPath = randomBoolean(); - final Settings indexSettings; final Settings nodeSettings; final String indexUUID = "0xDEADBEEF"; - Settings.Builder indexSettingsBuilder = Settings.builder() - .put(IndexMetaData.SETTING_INDEX_UUID, indexUUID) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT); final Path customPath; + final String customDataPath; if (useCustomDataPath) { final Path path = createTempDir(); - indexSettings = indexSettingsBuilder.put(IndexMetaData.SETTING_DATA_PATH, "custom").build(); + customDataPath = "custom"; nodeSettings = Settings.builder().put(Environment.PATH_SHARED_DATA_SETTING.getKey(), path.toAbsolutePath().toAbsolutePath()) .build(); customPath = path.resolve("custom").resolve("0"); } else { customPath = null; - indexSettings = indexSettingsBuilder.build(); + customDataPath = ""; nodeSettings = Settings.EMPTY; } try (NodeEnvironment env = newNodeEnvironment(nodeSettings)) { @@ -131,8 +113,7 @@ public void testGetRootPaths() throws IOException { Path path = randomFrom(paths); ShardStateMetaData.FORMAT.writeAndCleanup( new ShardStateMetaData(true, indexUUID, AllocationId.newInitializing()), path); - ShardPath shardPath = ShardPath.loadShardPath(logger, env, shardId, - IndexSettingsModule.newIndexSettings(shardId.getIndex(), indexSettings, nodeSettings)); + ShardPath shardPath = ShardPath.loadShardPath(logger, env, shardId, customDataPath); boolean found = false; for (Path p : env.nodeDataPaths()) { if (p.equals(shardPath.getRootStatePath())) { diff --git a/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java b/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java index 1ed0538288d6b..bd35f0f1783ca 100644 --- a/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java +++ b/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java @@ -248,7 +248,8 @@ public void testDeleteIndexStore() throws Exception { assertHitCount(client().prepareSearch("test").get(), 1); IndexMetaData secondMetaData = clusterService.state().metaData().index("test"); assertAcked(client().admin().indices().prepareClose("test")); - ShardPath path = ShardPath.loadShardPath(logger, getNodeEnvironment(), new ShardId(test.index(), 0), test.getIndexSettings()); + ShardPath path = ShardPath.loadShardPath(logger, getNodeEnvironment(), new ShardId(test.index(), 0), + test.getIndexSettings().customDataPath()); assertTrue(path.exists()); try { @@ -281,7 +282,8 @@ public void testPendingTasks() throws Exception { assertTrue(indexShard.routingEntry().started()); final ShardPath shardPath = indexShard.shardPath(); - assertEquals(ShardPath.loadShardPath(logger, getNodeEnvironment(), indexShard.shardId(), indexSettings), shardPath); + assertEquals(ShardPath.loadShardPath(logger, getNodeEnvironment(), indexShard.shardId(), indexSettings.customDataPath()), + shardPath); final IndicesService indicesService = getIndicesService(); expectThrows(ShardLockObtainFailedException.class, () -> From 71b102970d8f5a9f0de11b4905476e700b3663a4 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Tue, 17 Dec 2019 14:02:06 +0100 Subject: [PATCH 233/686] Optimize composite aggregation based on index sorting (#48399) Co-authored-by: Daniel Huang This is a spinoff of #48130 that generalizes the proposal to allow early termination with the composite aggregation when leading sources match a prefix or the entire index sort specification. In such case the composite aggregation can use the index sort natural order to early terminate the collection when it reaches a composite key that is greater than the bottom of the queue. The optimization is also applicable when a query other than match_all is provided. However the optimization is deactivated for sources that match the index sort in the following cases: * Multi-valued source, in such case early termination is not possible. * missing_bucket is set to true --- .../bucket/composite-aggregation.asciidoc | 125 +++++++++- .../CompositeAggregationBuilder.java | 3 +- .../bucket/composite/CompositeAggregator.java | 197 +++++++++++++--- .../CompositeValuesCollectorQueue.java | 60 +++-- .../CompositeValuesSourceBuilder.java | 2 +- .../CompositeValuesSourceConfig.java | 14 +- .../DateHistogramValuesSourceBuilder.java | 3 +- .../GeoTileGridValuesSourceBuilder.java | 3 +- .../HistogramValuesSourceBuilder.java | 3 +- .../bucket/composite/InternalComposite.java | 27 ++- .../bucket/composite/SortedDocsProducer.java | 3 +- .../composite/TermsValuesSourceBuilder.java | 2 +- .../bucket/terms/TermsAggregationBuilder.java | 5 + .../searchafter/SearchAfterBuilder.java | 3 +- .../composite/CompositeAggregatorTests.java | 222 ++++++++++++++---- .../CompositeValuesCollectorQueueTests.java | 73 ++++-- .../composite/InternalCompositeTests.java | 4 +- .../aggregations/AggregatorTestCase.java | 44 +++- 18 files changed, 662 insertions(+), 131 deletions(-) diff --git a/docs/reference/aggregations/bucket/composite-aggregation.asciidoc b/docs/reference/aggregations/bucket/composite-aggregation.asciidoc index bcc79fd989aaa..dd9aea6be3e90 100644 --- a/docs/reference/aggregations/bucket/composite-aggregation.asciidoc +++ b/docs/reference/aggregations/bucket/composite-aggregation.asciidoc @@ -116,6 +116,7 @@ Example: -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -134,6 +135,7 @@ Like the `terms` aggregation it is also possible to use a script to create the v -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -168,6 +170,7 @@ Example: -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -186,6 +189,7 @@ The values are built from a numeric field or a script that return numerical valu -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -218,6 +222,7 @@ is specified by date/time expression: -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -247,6 +252,7 @@ the format specified with the format parameter: -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -289,6 +295,7 @@ For example: -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -311,6 +318,7 @@ in the composite buckets. -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -340,6 +348,7 @@ For example: -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -366,6 +375,7 @@ It is possible to include them in the response by setting `missing_bucket` to -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -391,7 +401,7 @@ first 10 composite buckets created from the values source. The response contains the values for each composite bucket in an array containing the values extracted from each value source. -==== After +==== Pagination If the number of composite buckets is too high (or unknown) to be returned in a single response it is possible to split the retrieval in multiple requests. @@ -405,6 +415,7 @@ For example: -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -470,6 +481,7 @@ round of result can be retrieved with: -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { @@ -487,6 +499,116 @@ GET /_search <1> Should restrict the aggregation to buckets that sort **after** the provided values. +==== Early termination + +For optimal performance the <> should be set on the index so that it matches +parts or fully the source order in the composite aggregation. +For instance the following index sort: + +[source,console] +-------------------------------------------------- +PUT twitter +{ + "settings" : { + "index" : { + "sort.field" : ["username", "timestamp"], <1> + "sort.order" : ["asc", "desc"] <2> + } + }, + "mappings": { + "properties": { + "username": { + "type": "keyword", + "doc_values": true + }, + "timestamp": { + "type": "date" + } + } + } +} +-------------------------------------------------- + +<1> This index is sorted by `username` first then by `timestamp`. +<2> ... in ascending order for the `username` field and in descending order for the `timestamp` field. + +.. could be used to optimize these composite aggregations: + +[source,console] +-------------------------------------------------- +GET /_search +{ + "size": 0, + "aggs" : { + "my_buckets": { + "composite" : { + "sources" : [ + { "user_name": { "terms" : { "field": "user_name" } } } <1> + ] + } + } + } +} +-------------------------------------------------- + +<1> `user_name` is a prefix of the index sort and the order matches (`asc`). + +[source,console] +-------------------------------------------------- +GET /_search +{ + "size": 0, + "aggs" : { + "my_buckets": { + "composite" : { + "sources" : [ + { "user_name": { "terms" : { "field": "user_name" } } }, <1> + { "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d", "order": "desc" } } } <2> + ] + } + } + } +} +-------------------------------------------------- + +<1> `user_name` is a prefix of the index sort and the order matches (`asc`). +<2> `timestamp` matches also the prefix and the order matches (`desc`). + +In order to optimize the early termination it is advised to set `track_total_hits` in the request +to `false`. The number of total hits that match the request can be retrieved on the first request +and it would be costly to compute this number on every page: + +[source,console] +-------------------------------------------------- +GET /_search +{ + "size": 0, + "track_total_hits": false, + "aggs" : { + "my_buckets": { + "composite" : { + "sources" : [ + { "user_name": { "terms" : { "field": "user_name" } } }, + { "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d", "order": "desc" } } } + ] + } + } + } +} +-------------------------------------------------- + +Note that the order of the source is important, in the example below switching the `user_name` with the `timestamp` +would deactivate the sort optimization since this configuration wouldn't match the index sort specification. +If the order of sources do not matter for your use case you can follow these simple guidelines: + + * Put the fields with the highest cardinality first. + * Make sure that the order of the field matches the order of the index sort. + * Put multi-valued fields last since they cannot be used for early termination. + +WARNING: <> can slowdown indexing, it is very important to test index sorting +with your specific use case and dataset to ensure that it matches your requirement. If it doesn't note that `composite` +aggregations will also try to early terminate on non-sorted indices if the query matches all document (`match_all` query). + ==== Sub-aggregations Like any `multi-bucket` aggregations the `composite` aggregation can hold sub-aggregations. @@ -499,6 +621,7 @@ per composite bucket: -------------------------------------------------- GET /_search { + "size": 0, "aggs" : { "my_buckets": { "composite" : { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregationBuilder.java index b3712e231fded..243d1057bbf4a 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregationBuilder.java @@ -235,7 +235,8 @@ protected AggregatorFactory doBuild(QueryShardContext queryShardContext, Aggrega } else { afterKey = null; } - return new CompositeAggregationFactory(name, queryShardContext, parent, subfactoriesBuilder, metaData, size, configs, afterKey); + return new CompositeAggregationFactory(name, queryShardContext, parent, subfactoriesBuilder, metaData, size, + configs, afterKey); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregator.java index 4effb22f30cb2..0e01615492939 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregator.java @@ -20,18 +20,28 @@ package org.elasticsearch.search.aggregations.bucket.composite; import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.DocValues; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.SortedNumericDocValues; +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.queries.SearchAfterSortedDocQuery; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.CollectionTerminatedException; import org.apache.lucene.search.DocIdSet; import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.FieldDoc; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreMode; import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; import org.apache.lucene.search.Weight; import org.apache.lucene.util.RoaringDocIdSet; import org.elasticsearch.common.lease.Releasables; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.index.IndexSortConfig; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; @@ -46,6 +56,8 @@ import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.search.searchafter.SearchAfterBuilder; +import org.elasticsearch.search.sort.SortAndFormats; import java.io.IOException; import java.util.ArrayList; @@ -60,11 +72,12 @@ final class CompositeAggregator extends BucketsAggregator { private final int size; - private final SortedDocsProducer sortedDocsProducer; private final List sourceNames; private final int[] reverseMuls; private final List formats; + private final CompositeKey rawAfterKey; + private final CompositeValuesSourceConfig[] sourceConfigs; private final SingleDimensionValuesSource[] sources; private final CompositeValuesCollectorQueue queue; @@ -73,6 +86,8 @@ final class CompositeAggregator extends BucketsAggregator { private RoaringDocIdSet.Builder docIdSetBuilder; private BucketCollector deferredCollectors; + private boolean earlyTerminated; + CompositeAggregator(String name, AggregatorFactories factories, SearchContext context, Aggregator parent, List pipelineAggregators, Map metaData, int size, CompositeValuesSourceConfig[] sourceConfigs, CompositeKey rawAfterKey) throws IOException { @@ -89,11 +104,12 @@ final class CompositeAggregator extends BucketsAggregator { " to: [" + bucketLimit + "] but was [" + size + "]. This limit can be set by changing the [" + MAX_BUCKET_SETTING.getKey() + "] cluster level setting.", bucketLimit); } + this.sourceConfigs = sourceConfigs; for (int i = 0; i < sourceConfigs.length; i++) { this.sources[i] = createValuesSource(context.bigArrays(), context.searcher().getIndexReader(), sourceConfigs[i], size); } this.queue = new CompositeValuesCollectorQueue(context.bigArrays(), sources, size, rawAfterKey); - this.sortedDocsProducer = sources[0].createSortedDocsProducerOrNull(context.searcher().getIndexReader(), context.query()); + this.rawAfterKey = rawAfterKey; } @Override @@ -121,7 +137,6 @@ protected void doPostCollection() throws IOException { public InternalAggregation buildAggregation(long zeroBucket) throws IOException { assert zeroBucket == 0L; consumeBucketsAndMaybeBreak(queue.size()); - if (deferredCollectors != NO_OP_COLLECTOR) { // Replay all documents that contain at least one top bucket (collected during the first pass). runDeferredCollections(); @@ -138,13 +153,13 @@ public InternalAggregation buildAggregation(long zeroBucket) throws IOException } CompositeKey lastBucket = num > 0 ? buckets[num-1].getRawKey() : null; return new InternalComposite(name, size, sourceNames, formats, Arrays.asList(buckets), lastBucket, reverseMuls, - pipelineAggregators(), metaData()); + earlyTerminated, pipelineAggregators(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { return new InternalComposite(name, size, sourceNames, formats, Collections.emptyList(), null, reverseMuls, - pipelineAggregators(), metaData()); + false, pipelineAggregators(), metaData()); } private void finishLeaf() { @@ -156,58 +171,179 @@ private void finishLeaf() { } } + /** Return true if the provided field may have multiple values per document in the leaf **/ + private boolean isMaybeMultivalued(LeafReaderContext context, SortField sortField) throws IOException { + SortField.Type type = IndexSortConfig.getSortFieldType(sortField); + switch (type) { + case STRING: + final SortedSetDocValues v1 = context.reader().getSortedSetDocValues(sortField.getField()); + return v1 != null && DocValues.unwrapSingleton(v1) == null; + + case DOUBLE: + case FLOAT: + case LONG: + case INT: + final SortedNumericDocValues v2 = context.reader().getSortedNumericDocValues(sortField.getField()); + return v2 != null && DocValues.unwrapSingleton(v2) == null; + + default: + // we have no clue whether the field is multi-valued or not so we assume it is. + return true; + } + } + + /** + * Returns the {@link Sort} prefix that is eligible to index sort + * optimization and null if index sort is not applicable. + */ + private Sort buildIndexSortPrefix(LeafReaderContext context) throws IOException { + Sort indexSort = context.reader().getMetaData().getSort(); + if (indexSort == null) { + return null; + } + List sortFields = new ArrayList<>(); + for (int i = 0; i < indexSort.getSort().length; i++) { + CompositeValuesSourceConfig sourceConfig = sourceConfigs[i]; + SingleDimensionValuesSource source = sources[i]; + SortField indexSortField = indexSort.getSort()[i]; + if (source.fieldType == null + // TODO: can we handle missing bucket when using index sort optimization ? + || source.missingBucket + || indexSortField.getField().equals(source.fieldType.name()) == false + || isMaybeMultivalued(context, indexSortField) + || sourceConfig.hasScript()) { + break; + } + + if (indexSortField.getReverse() != (source.reverseMul == -1)) { + if (i == 0) { + // the leading index sort matches the leading source field but the order is reversed + // so we don't check the other sources. + return new Sort(indexSortField); + } + break; + } + sortFields.add(indexSortField); + } + return sortFields.isEmpty() ? null : new Sort(sortFields.toArray(new SortField[0])); + } + + /** + * Return the number of leading sources that match the index sort. + * + * @param indexSortPrefix The index sort prefix that matches the sources + * @return The length of the index sort prefix if the sort order matches + * or -1 if the leading index sort is in the reverse order of the + * leading source. A value of 0 indicates that the index sort is + * not applicable. + */ + private int computeSortPrefixLen(Sort indexSortPrefix) { + if (indexSortPrefix == null) { + return 0; + } + if (indexSortPrefix.getSort()[0].getReverse() != (sources[0].reverseMul == -1)) { + assert indexSortPrefix.getSort().length == 1; + return -1; + } else { + return indexSortPrefix.getSort().length; + } + } + + private void processLeafFromQuery(LeafReaderContext ctx, Sort indexSortPrefix) throws IOException { + DocValueFormat[] formats = new DocValueFormat[indexSortPrefix.getSort().length]; + for (int i = 0; i < formats.length; i++) { + formats[i] = sources[i].format; + } + FieldDoc fieldDoc = SearchAfterBuilder.buildFieldDoc(new SortAndFormats(indexSortPrefix, formats), + Arrays.copyOfRange(rawAfterKey.values(), 0, formats.length)); + if (indexSortPrefix.getSort().length < sources.length) { + // include all docs that belong to the partial bucket + fieldDoc.doc = 0; + } + BooleanQuery newQuery = new BooleanQuery.Builder() + .add(context.query(), BooleanClause.Occur.MUST) + .add(new SearchAfterSortedDocQuery(indexSortPrefix, fieldDoc), BooleanClause.Occur.FILTER) + .build(); + Weight weight = context.searcher().createWeight(context.searcher().rewrite(newQuery), ScoreMode.COMPLETE_NO_SCORES, 1f); + Scorer scorer = weight.scorer(ctx); + if (scorer != null) { + DocIdSetIterator docIt = scorer.iterator(); + final LeafBucketCollector inner = queue.getLeafCollector(ctx, + getFirstPassCollector(docIdSetBuilder, indexSortPrefix.getSort().length)); + inner.setScorer(scorer); + while (docIt.nextDoc() != DocIdSetIterator.NO_MORE_DOCS) { + inner.collect(docIt.docID()); + } + } + } + @Override protected LeafBucketCollector getLeafCollector(LeafReaderContext ctx, LeafBucketCollector sub) throws IOException { finishLeaf(); + boolean fillDocIdSet = deferredCollectors != NO_OP_COLLECTOR; + + Sort indexSortPrefix = buildIndexSortPrefix(ctx); + int sortPrefixLen = computeSortPrefixLen(indexSortPrefix); + + SortedDocsProducer sortedDocsProducer = sortPrefixLen == 0 ? + sources[0].createSortedDocsProducerOrNull(ctx.reader(), context.query()) : null; if (sortedDocsProducer != null) { - /* - The producer will visit documents sorted by the leading source of the composite definition - and terminates when the leading source value is guaranteed to be greater than the lowest - composite bucket in the queue. - */ + // Visit documents sorted by the leading source of the composite definition and terminates + // when the leading source value is guaranteed to be greater than the lowest composite bucket + // in the queue. DocIdSet docIdSet = sortedDocsProducer.processLeaf(context.query(), queue, ctx, fillDocIdSet); if (fillDocIdSet) { entries.add(new Entry(ctx, docIdSet)); } - - /* - We can bypass search entirely for this segment, all the processing has been done in the previous call. - Throwing this exception will terminate the execution of the search for this root aggregation, - see {@link org.apache.lucene.search.MultiCollector} for more details on how we handle early termination in aggregations. - */ + // We can bypass search entirely for this segment, the processing is done in the previous call. + // Throwing this exception will terminate the execution of the search for this root aggregation, + // see {@link MultiCollector} for more details on how we handle early termination in aggregations. + earlyTerminated = true; throw new CollectionTerminatedException(); } else { if (fillDocIdSet) { currentLeaf = ctx; docIdSetBuilder = new RoaringDocIdSet.Builder(ctx.reader().maxDoc()); } - final LeafBucketCollector inner = queue.getLeafCollector(ctx, getFirstPassCollector(docIdSetBuilder)); - return new LeafBucketCollector() { - @Override - public void collect(int doc, long zeroBucket) throws IOException { - assert zeroBucket == 0L; - inner.collect(doc); - } - }; + if (rawAfterKey != null && sortPrefixLen > 0) { + // We have an after key and index sort is applicable so we jump directly to the doc + // that is after the index sort prefix using the rawAfterKey and we start collecting + // document from there. + processLeafFromQuery(ctx, indexSortPrefix); + throw new CollectionTerminatedException(); + } else { + final LeafBucketCollector inner = queue.getLeafCollector(ctx, getFirstPassCollector(docIdSetBuilder, sortPrefixLen)); + return new LeafBucketCollector() { + @Override + public void collect(int doc, long zeroBucket) throws IOException { + assert zeroBucket == 0L; + inner.collect(doc); + } + }; + } } } /** * The first pass selects the top composite buckets from all matching documents. */ - private LeafBucketCollector getFirstPassCollector(RoaringDocIdSet.Builder builder) { + private LeafBucketCollector getFirstPassCollector(RoaringDocIdSet.Builder builder, int indexSortPrefix) { return new LeafBucketCollector() { int lastDoc = -1; @Override public void collect(int doc, long bucket) throws IOException { - int slot = queue.addIfCompetitive(); - if (slot != -1) { - if (builder != null && lastDoc != doc) { - builder.add(doc); - lastDoc = doc; + try { + if (queue.addIfCompetitive(indexSortPrefix)) { + if (builder != null && lastDoc != doc) { + builder.add(doc); + lastDoc = doc; + } } + } catch (CollectionTerminatedException exc) { + earlyTerminated = true; + throw exc; } } }; @@ -274,7 +410,6 @@ public void collect(int doc, long zeroBucket) throws IOException { private SingleDimensionValuesSource createValuesSource(BigArrays bigArrays, IndexReader reader, CompositeValuesSourceConfig config, int size) { - final int reverseMul = config.reverseMul(); if (config.valuesSource() instanceof ValuesSource.Bytes.WithOrdinals && reader instanceof DirectoryReader) { ValuesSource.Bytes.WithOrdinals vs = (ValuesSource.Bytes.WithOrdinals) config.valuesSource(); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueue.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueue.java index 58887d9e6a2dc..93511498e2258 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueue.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueue.java @@ -20,6 +20,7 @@ package org.elasticsearch.search.aggregations.bucket.composite; import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.CollectionTerminatedException; import org.apache.lucene.util.PriorityQueue; import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.lease.Releasables; @@ -63,6 +64,7 @@ public int hashCode() { private final int maxSize; private final Map map; private final SingleDimensionValuesSource[] arrays; + private IntArray docCounts; private boolean afterKeyIsSet = false; @@ -153,7 +155,7 @@ int compare(int slot1, int slot2) { cmp = arrays[i].compare(slot1, slot2); } if (cmp != 0) { - return cmp; + return cmp > 0 ? i+1 : -(i+1); } } return 0; @@ -244,27 +246,57 @@ LeafBucketCollector getLeafCollector(Comparable forceLeadSourceValue, /** * Check if the current candidate should be added in the queue. - * @return The target slot of the candidate or -1 is the candidate is not competitive. + * @return true if the candidate is competitive (added or already in the queue). + */ + boolean addIfCompetitive() { + return addIfCompetitive(0); + } + + + /** + * Add or update the current composite key in the queue if the values are competitive. + * + * @param indexSortSourcePrefix 0 if the index sort is null or doesn't match any of the sources field, + * a value greater than 0 indicates the prefix len of the sources that match the index sort + * and a negative value indicates that the index sort match the source field but the order is reversed. + * @return true if the candidate is competitive (added or already in the queue). + * + * @throws CollectionTerminatedException if the current collection can be terminated early due to index sorting. */ - int addIfCompetitive() { + boolean addIfCompetitive(int indexSortSourcePrefix) { // checks if the candidate key is competitive Integer topSlot = compareCurrent(); if (topSlot != null) { // this key is already in the top N, skip it docCounts.increment(topSlot, 1); - return topSlot; + return true; } - if (afterKeyIsSet && compareCurrentWithAfter() <= 0) { - // this key is greater than the top value collected in the previous round, skip it - return -1; + if (afterKeyIsSet) { + int cmp = compareCurrentWithAfter(); + if (cmp <= 0) { + if (indexSortSourcePrefix < 0 && cmp == indexSortSourcePrefix) { + // the leading index sort is in the reverse order of the leading source + // so we can early terminate when we reach a document that is smaller + // than the after key (collected on a previous page). + throw new CollectionTerminatedException(); + } + // key was collected on a previous page, skip it (>= afterKey). + return false; + } } - if (size() >= maxSize - // the tree map is full, check if the candidate key should be kept - && compare(CANDIDATE_SLOT, top()) > 0) { - // the candidate key is not competitive, skip it - return -1; + if (size() >= maxSize) { + // the tree map is full, check if the candidate key should be kept + int cmp = compare(CANDIDATE_SLOT, top()); + if (cmp > 0) { + if (cmp <= indexSortSourcePrefix) { + // index sort guarantees that there is no key greater or equal than the + // current one in the subsequent documents so we can early terminate. + throw new CollectionTerminatedException(); + } + // the candidate key is not competitive, skip it. + return false; + } } - // the candidate key is competitive final int newSlot; if (size() >= maxSize) { @@ -280,7 +312,7 @@ && compare(CANDIDATE_SLOT, top()) > 0) { copyCurrent(newSlot); map.put(new Slot(newSlot), newSlot); add(newSlot); - return newSlot; + return true; } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesSourceBuilder.java index 70687cf9efe0c..d113ded687f81 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesSourceBuilder.java @@ -200,7 +200,7 @@ public ValueType valueType() { } /** - * If true an explicit `null bucket will represent documents with missing values. + * If true an explicit null bucket will represent documents with missing values. */ @SuppressWarnings("unchecked") public AB missingBucket(boolean missingBucket) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesSourceConfig.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesSourceConfig.java index bf88285c190c8..5c9378d44eff4 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesSourceConfig.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesSourceConfig.java @@ -33,23 +33,28 @@ class CompositeValuesSourceConfig { private final DocValueFormat format; private final int reverseMul; private final boolean missingBucket; + private final boolean hasScript; /** * Creates a new {@link CompositeValuesSourceConfig}. + * * @param name The name of the source. * @param fieldType The field type or null if the source is a script. * @param vs The underlying {@link ValuesSource}. * @param format The {@link DocValueFormat} of this source. * @param order The sort order associated with this source. + * @param missingBucket If true an explicit null bucket will represent documents with missing values. + * @param hasScript true if the source contains a script that can change the value. */ CompositeValuesSourceConfig(String name, @Nullable MappedFieldType fieldType, ValuesSource vs, DocValueFormat format, - SortOrder order, boolean missingBucket) { + SortOrder order, boolean missingBucket, boolean hasScript) { this.name = name; this.fieldType = fieldType; this.vs = vs; this.format = format; this.reverseMul = order == SortOrder.ASC ? 1 : -1; this.missingBucket = missingBucket; + this.hasScript = hasScript; } /** @@ -88,6 +93,13 @@ boolean missingBucket() { return missingBucket; } + /** + * Returns true if the source contains a script that can change the value. + */ + boolean hasScript() { + return hasScript; + } + /** * The sort order for the values source (e.g. -1 for descending and 1 for ascending). */ diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/DateHistogramValuesSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/DateHistogramValuesSourceBuilder.java index f1c1f5502dfd4..564399d4c2647 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/DateHistogramValuesSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/DateHistogramValuesSourceBuilder.java @@ -228,7 +228,8 @@ protected CompositeValuesSourceConfig innerBuild(QueryShardContext queryShardCon // is specified in the builder. final DocValueFormat docValueFormat = format() == null ? DocValueFormat.RAW : config.format(); final MappedFieldType fieldType = config.fieldContext() != null ? config.fieldContext().fieldType() : null; - return new CompositeValuesSourceConfig(name, fieldType, vs, docValueFormat, order(), missingBucket()); + return new CompositeValuesSourceConfig(name, fieldType, vs, docValueFormat, order(), + missingBucket(), config.script() != null); } else { throw new IllegalArgumentException("invalid source, expected numeric, got " + orig.getClass().getSimpleName()); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java index 17a5b3c0e9993..b6f2b2788cd25 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java @@ -113,7 +113,8 @@ protected CompositeValuesSourceConfig innerBuild(QueryShardContext queryShardCon // is specified in the builder. final MappedFieldType fieldType = config.fieldContext() != null ? config.fieldContext().fieldType() : null; CellIdSource cellIdSource = new CellIdSource(geoPoint, precision, GeoTileUtils::longEncode); - return new CompositeValuesSourceConfig(name, fieldType, cellIdSource, DocValueFormat.GEOTILE, order(), missingBucket()); + return new CompositeValuesSourceConfig(name, fieldType, cellIdSource, DocValueFormat.GEOTILE, order(), + missingBucket(), script() != null); } else { throw new IllegalArgumentException("invalid source, expected geo_point, got " + orig.getClass().getSimpleName()); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/HistogramValuesSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/HistogramValuesSourceBuilder.java index daafa6f14418e..aa15d5a6947d9 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/HistogramValuesSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/HistogramValuesSourceBuilder.java @@ -119,7 +119,8 @@ protected CompositeValuesSourceConfig innerBuild(QueryShardContext queryShardCon ValuesSource.Numeric numeric = (ValuesSource.Numeric) orig; final HistogramValuesSource vs = new HistogramValuesSource(numeric, interval); final MappedFieldType fieldType = config.fieldContext() != null ? config.fieldContext().fieldType() : null; - return new CompositeValuesSourceConfig(name, fieldType, vs, config.format(), order(), missingBucket()); + return new CompositeValuesSourceConfig(name, fieldType, vs, config.format(), order(), + missingBucket(), script() != null); } else { throw new IllegalArgumentException("invalid source, expected numeric, got " + orig.getClass().getSimpleName()); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java index 243ae557bfa2c..d3d4c34953216 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java @@ -20,6 +20,7 @@ package org.elasticsearch.search.aggregations.bucket.composite; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -53,8 +54,10 @@ public class InternalComposite private final List sourceNames; private final List formats; + private final boolean earlyTerminated; + InternalComposite(String name, int size, List sourceNames, List formats, - List buckets, CompositeKey afterKey, int[] reverseMuls, + List buckets, CompositeKey afterKey, int[] reverseMuls, boolean earlyTerminated, List pipelineAggregators, Map metaData) { super(name, pipelineAggregators, metaData); this.sourceNames = sourceNames; @@ -63,6 +66,7 @@ public class InternalComposite this.afterKey = afterKey; this.size = size; this.reverseMuls = reverseMuls; + this.earlyTerminated = earlyTerminated; } public InternalComposite(StreamInput in) throws IOException { @@ -75,7 +79,8 @@ public InternalComposite(StreamInput in) throws IOException { } this.reverseMuls = in.readIntArray(); this.buckets = in.readList((input) -> new InternalBucket(input, sourceNames, formats, reverseMuls)); - this.afterKey = in.readBoolean() ? new CompositeKey(in) : null; + this.afterKey = in.readOptionalWriteable(CompositeKey::new); + this.earlyTerminated = in.getVersion().onOrAfter(Version.V_8_0_0) ? in.readBoolean() : false; } @Override @@ -87,9 +92,9 @@ protected void doWriteTo(StreamOutput out) throws IOException { } out.writeIntArray(reverseMuls); out.writeList(buckets); - out.writeBoolean(afterKey != null); - if (afterKey != null) { - afterKey.writeTo(out); + out.writeOptionalWriteable(afterKey); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeBoolean(earlyTerminated); } } @@ -111,7 +116,7 @@ public InternalComposite create(List newBuckets) { * to be able to retrieve the next page even if all buckets have been filtered. */ return new InternalComposite(name, size, sourceNames, formats, newBuckets, afterKey, - reverseMuls, pipelineAggregators(), getMetaData()); + reverseMuls, earlyTerminated, pipelineAggregators(), getMetaData()); } @Override @@ -137,6 +142,11 @@ public Map afterKey() { return null; } + // Visible for tests + boolean isTerminatedEarly() { + return earlyTerminated; + } + // Visible for tests int[] getReverseMuls() { return reverseMuls; @@ -145,8 +155,10 @@ int[] getReverseMuls() { @Override public InternalAggregation reduce(List aggregations, ReduceContext reduceContext) { PriorityQueue pq = new PriorityQueue<>(aggregations.size()); + boolean earlyTerminated = false; for (InternalAggregation agg : aggregations) { InternalComposite sortedAgg = (InternalComposite) agg; + earlyTerminated |= sortedAgg.earlyTerminated; BucketIterator it = new BucketIterator(sortedAgg.buckets); if (it.next() != null) { pq.add(it); @@ -178,7 +190,8 @@ public InternalAggregation reduce(List aggregations, Reduce result.add(reduceBucket); } final CompositeKey lastKey = result.size() > 0 ? result.get(result.size()-1).getRawKey() : null; - return new InternalComposite(name, size, sourceNames, formats, result, lastKey, reverseMuls, pipelineAggregators(), metaData); + return new InternalComposite(name, size, sourceNames, formats, result, lastKey, reverseMuls, + earlyTerminated, pipelineAggregators(), metaData); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/SortedDocsProducer.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/SortedDocsProducer.java index 63530a4eed6ed..01e2c0a3ae5b0 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/SortedDocsProducer.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/SortedDocsProducer.java @@ -66,8 +66,7 @@ protected boolean processBucket(CompositeValuesCollectorQueue queue, LeafReaderC @Override public void collect(int doc, long bucket) throws IOException { hasCollected[0] = true; - int slot = queue.addIfCompetitive(); - if (slot != -1) { + if (queue.addIfCompetitive()) { topCompositeCollected[0]++; if (adder != null && doc != lastDoc) { if (remainingBits == 0) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/TermsValuesSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/TermsValuesSourceBuilder.java index 8d02eb4b19d87..cd88a56614ec5 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/TermsValuesSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/TermsValuesSourceBuilder.java @@ -85,6 +85,6 @@ protected CompositeValuesSourceConfig innerBuild(QueryShardContext queryShardCon } else { format = config.format(); } - return new CompositeValuesSourceConfig(name, fieldType, vs, format, order(), missingBucket()); + return new CompositeValuesSourceConfig(name, fieldType, vs, format, order(), missingBucket(), script() != null); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregationBuilder.java index 228a3ca7ee2b1..b7a01099597b1 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregationBuilder.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.Aggregator.SubAggCollectionMode; @@ -385,4 +386,8 @@ public String getType() { return NAME; } + @Override + protected AggregationBuilder doRewrite(QueryRewriteContext queryShardContext) throws IOException { + return super.doRewrite(queryShardContext); + } } diff --git a/server/src/main/java/org/elasticsearch/search/searchafter/SearchAfterBuilder.java b/server/src/main/java/org/elasticsearch/search/searchafter/SearchAfterBuilder.java index 304a639a8981a..6c3ac160bc661 100644 --- a/server/src/main/java/org/elasticsearch/search/searchafter/SearchAfterBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/searchafter/SearchAfterBuilder.java @@ -184,7 +184,8 @@ private static Object convertValueFromSortType(String fieldName, SortField.Type if (value instanceof Number) { return ((Number) value).longValue(); } - return Long.parseLong(value.toString()); + return format.parseLong(value.toString(), false, + () -> { throw new IllegalStateException("now() is not allowed in [search_after] key"); }); case FLOAT: if (value instanceof Number) { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregatorTests.java index 1520dfde8a116..601154234e792 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregatorTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.bucket.composite; +import org.apache.lucene.analysis.MockAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.DoublePoint; import org.apache.lucene.document.Field; @@ -31,17 +32,28 @@ import org.apache.lucene.document.StringField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.index.Term; import org.apache.lucene.search.DocValuesFieldExistsQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.SortedNumericSortField; +import org.apache.lucene.search.SortedSetSortField; +import org.apache.lucene.search.TermQuery; import org.apache.lucene.store.Directory; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.NumericUtils; +import org.apache.lucene.util.TestUtil; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.time.DateFormatters; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.ContentPath; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.index.mapper.GeoPointFieldMapper; @@ -63,6 +75,7 @@ import org.elasticsearch.search.aggregations.metrics.TopHitsAggregationBuilder; import org.elasticsearch.search.aggregations.support.ValueType; import org.elasticsearch.search.sort.SortOrder; +import org.elasticsearch.test.IndexSettingsModule; import org.junit.After; import org.junit.Before; @@ -82,12 +95,13 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; -public class CompositeAggregatorTests extends AggregatorTestCase { +public class CompositeAggregatorTests extends AggregatorTestCase { private static MappedFieldType[] FIELD_TYPES; @Override @@ -109,6 +123,7 @@ public void setUp() throws Exception { DateFieldMapper.Builder builder = new DateFieldMapper.Builder("date"); builder.docValues(true); + builder.format("yyyy-MM-dd||epoch_millis"); DateFieldMapper fieldMapper = builder.build(new Mapper.BuilderContext(createIndexSettings().getSettings(), new ContentPath(0))); FIELD_TYPES[3] = fieldMapper.fieldType(); @@ -419,7 +434,7 @@ public void testWithKeywordMissingAfter() throws Exception { ); } - public void testWithKeywordDesc() throws Exception { + public void testWithKeywordDesc() throws Exception { final List>> dataset = new ArrayList<>(); dataset.addAll( Arrays.asList( @@ -485,19 +500,19 @@ public void testMultiValuedWithKeyword() throws Exception { return new CompositeAggregationBuilder("name", Collections.singletonList(terms)); }, (result) -> { - assertEquals(5, result.getBuckets().size()); - assertEquals("{keyword=z}", result.afterKey().toString()); - assertEquals("{keyword=a}", result.getBuckets().get(0).getKeyAsString()); - assertEquals(2L, result.getBuckets().get(0).getDocCount()); - assertEquals("{keyword=b}", result.getBuckets().get(1).getKeyAsString()); - assertEquals(2L, result.getBuckets().get(1).getDocCount()); - assertEquals("{keyword=c}", result.getBuckets().get(2).getKeyAsString()); - assertEquals(1L, result.getBuckets().get(2).getDocCount()); - assertEquals("{keyword=d}", result.getBuckets().get(3).getKeyAsString()); - assertEquals(1L, result.getBuckets().get(3).getDocCount()); - assertEquals("{keyword=z}", result.getBuckets().get(4).getKeyAsString()); - assertEquals(1L, result.getBuckets().get(4).getDocCount()); - } + assertEquals(5, result.getBuckets().size()); + assertEquals("{keyword=z}", result.afterKey().toString()); + assertEquals("{keyword=a}", result.getBuckets().get(0).getKeyAsString()); + assertEquals(2L, result.getBuckets().get(0).getDocCount()); + assertEquals("{keyword=b}", result.getBuckets().get(1).getKeyAsString()); + assertEquals(2L, result.getBuckets().get(1).getDocCount()); + assertEquals("{keyword=c}", result.getBuckets().get(2).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(2).getDocCount()); + assertEquals("{keyword=d}", result.getBuckets().get(3).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(3).getDocCount()); + assertEquals("{keyword=z}", result.getBuckets().get(4).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(4).getDocCount()); + } ); testSearchCase(Arrays.asList(new MatchAllDocsQuery(), new DocValuesFieldExistsQuery("keyword")), dataset, @@ -589,10 +604,10 @@ public void testWithKeywordAndLong() throws Exception { ); testSearchCase(Arrays.asList(new MatchAllDocsQuery(), new DocValuesFieldExistsQuery("keyword")), dataset, () -> new CompositeAggregationBuilder("name", - Arrays.asList( - new TermsValuesSourceBuilder("keyword").field("keyword"), - new TermsValuesSourceBuilder("long").field("long") - ) + Arrays.asList( + new TermsValuesSourceBuilder("keyword").field("keyword"), + new TermsValuesSourceBuilder("long").field("long") + ) ), (result) -> { assertEquals(4, result.getBuckets().size()); @@ -610,11 +625,11 @@ public void testWithKeywordAndLong() throws Exception { testSearchCase(Arrays.asList(new MatchAllDocsQuery(), new DocValuesFieldExistsQuery("keyword")), dataset, () -> new CompositeAggregationBuilder("name", - Arrays.asList( - new TermsValuesSourceBuilder("keyword").field("keyword"), - new TermsValuesSourceBuilder("long").field("long") - ) - ).aggregateAfter(createAfterKey("keyword", "a", "long", 100L) + Arrays.asList( + new TermsValuesSourceBuilder("keyword").field("keyword"), + new TermsValuesSourceBuilder("long").field("long") + ) + ).aggregateAfter(createAfterKey("keyword", "a", "long", 100L) ), (result) -> { assertEquals(2, result.getBuckets().size()); @@ -942,7 +957,7 @@ public void testMultiValuedWithKeywordLongAndDouble() throws Exception { new TermsValuesSourceBuilder("double").field("double") ) ).aggregateAfter(createAfterKey("keyword", "a", "long", 100L, "double", 0.4d)) - ,(result) -> { + , (result) -> { assertEquals(10, result.getBuckets().size()); assertEquals("{keyword=z, long=0, double=0.09}", result.afterKey().toString()); assertEquals("{keyword=b, long=100, double=0.4}", result.getBuckets().get(0).getKeyAsString()); @@ -1152,8 +1167,9 @@ public void testThatDateHistogramFailsFormatAfter() throws IOException { return new CompositeAggregationBuilder("name", Collections.singletonList(histo)) .aggregateAfter(createAfterKey("date", "now")); }, - (result) -> {} - )); + (result) -> { + } + )); assertThat(exc.getCause(), instanceOf(IllegalArgumentException.class)); assertThat(exc.getCause().getMessage(), containsString("now() is not supported in [after] key")); @@ -1167,7 +1183,8 @@ public void testThatDateHistogramFailsFormatAfter() throws IOException { return new CompositeAggregationBuilder("name", Collections.singletonList(histo)) .aggregateAfter(createAfterKey("date", "1474329600000")); }, - (result) -> {} + (result) -> { + } )); assertThat(exc.getMessage(), containsString("failed to parse date field [1474329600000]")); assertWarnings("[interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future."); @@ -1486,7 +1503,7 @@ public void testWithKeywordAndDateHistogram() throws IOException { new DateHistogramValuesSourceBuilder("date_histo").field("date") .dateHistogramInterval(DateHistogramInterval.days(1)) ) - ).aggregateAfter(createAfterKey("keyword","c", "date_histo", 1474329600000L)) + ).aggregateAfter(createAfterKey("keyword", "c", "date_histo", 1474329600000L)) , (result) -> { assertEquals(4, result.getBuckets().size()); assertEquals("{keyword=z, date_histo=1474329600000}", result.afterKey().toString()); @@ -1668,7 +1685,7 @@ public void testDuplicateNames() { builders.add(new TermsValuesSourceBuilder("duplicate1").field("baz")); builders.add(new TermsValuesSourceBuilder("duplicate2").field("bar")); builders.add(new TermsValuesSourceBuilder("duplicate2").field("baz")); - new CompositeAggregationBuilder("foo", builders); + new CompositeAggregationBuilder("foo", builders); }); assertThat(e.getMessage(), equalTo("Composite source names must be unique, found duplicates: [duplicate2, duplicate1]")); } @@ -1705,7 +1722,7 @@ private , V extends Comparable> void testRandomTerms( List>> dataset = new ArrayList<>(); Set valuesSet = new HashSet<>(); - Map, AtomicLong> expectedDocCounts = new HashMap<> (); + Map, AtomicLong> expectedDocCounts = new HashMap<>(); for (int i = 0; i < numDocs; i++) { int numValues = randomIntBetween(1, 5); Set values = new HashSet<>(); @@ -1725,13 +1742,13 @@ private , V extends Comparable> void testRandomTerms( List> seen = new ArrayList<>(); AtomicBoolean finish = new AtomicBoolean(false); - int size = randomIntBetween(1, expected.size()); + int size = randomIntBetween(1, expected.size()); while (finish.get() == false) { testSearchCase(Arrays.asList(new MatchAllDocsQuery(), new DocValuesFieldExistsQuery(field)), dataset, () -> { Map afterKey = null; if (seen.size() > 0) { - afterKey = Collections.singletonMap(field, seen.get(seen.size()-1)); + afterKey = Collections.singletonMap(field, seen.get(seen.size() - 1)); } TermsValuesSourceBuilder source = new TermsValuesSourceBuilder(field).field(field); return new CompositeAggregationBuilder("name", Collections.singletonList(source)) @@ -1838,44 +1855,130 @@ public void testWithGeoPoint() throws Exception { ); } + public void testEarlyTermination() throws Exception { + final List>> dataset = new ArrayList<>(); + dataset.addAll( + Arrays.asList( + createDocument("keyword", "a", "long", 100L, "foo", "bar"), + createDocument("keyword", "c", "long", 100L, "foo", "bar"), + createDocument("keyword", "a", "long", 0L, "foo", "bar"), + createDocument("keyword", "d", "long", 10L, "foo", "bar"), + createDocument("keyword", "b", "long", 10L, "foo", "bar"), + createDocument("keyword", "c", "long", 10L, "foo", "bar"), + createDocument("keyword", "e", "long", 100L, "foo", "bar"), + createDocument("keyword", "e", "long", 10L, "foo", "bar") + ) + ); + + executeTestCase(true, false, new TermQuery(new Term("foo", "bar")), + dataset, + () -> + new CompositeAggregationBuilder("name", + Arrays.asList( + new TermsValuesSourceBuilder("keyword").field("keyword"), + new TermsValuesSourceBuilder("long").field("long") + )).aggregateAfter(createAfterKey("keyword", "b", "long", 10L)).size(2), + (result) -> { + assertEquals(2, result.getBuckets().size()); + assertEquals("{keyword=c, long=100}", result.afterKey().toString()); + assertEquals("{keyword=c, long=10}", result.getBuckets().get(0).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(0).getDocCount()); + assertEquals("{keyword=c, long=100}", result.getBuckets().get(1).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(1).getDocCount()); + assertTrue(result.isTerminatedEarly()); + } + ); + + // source field and index sorting config have different order + executeTestCase(true, false, new TermQuery(new Term("foo", "bar")), + dataset, + () -> + new CompositeAggregationBuilder("name", + Arrays.asList( + // reverse source order + new TermsValuesSourceBuilder("keyword").field("keyword").order(SortOrder.DESC), + new TermsValuesSourceBuilder("long").field("long").order(SortOrder.DESC) + ) + ).aggregateAfter(createAfterKey("keyword", "c", "long", 10L)).size(2), + (result) -> { + assertEquals(2, result.getBuckets().size()); + assertEquals("{keyword=a, long=100}", result.afterKey().toString()); + assertEquals("{keyword=b, long=10}", result.getBuckets().get(0).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(0).getDocCount()); + assertEquals("{keyword=a, long=100}", result.getBuckets().get(1).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(1).getDocCount()); + assertTrue(result.isTerminatedEarly()); + } + ); + } + private void testSearchCase(List queries, List>> dataset, Supplier create, Consumer verify) throws IOException { for (Query query : queries) { - executeTestCase(false, query, dataset, create, verify); - executeTestCase(true, query, dataset, create, verify); + executeTestCase(false, false, query, dataset, create, verify); + executeTestCase(false, true, query, dataset, create, verify); + executeTestCase(true, true, query, dataset, create, verify); } } - private void executeTestCase(boolean reduced, + private void executeTestCase(boolean useIndexSort, + boolean reduced, Query query, List>> dataset, Supplier create, Consumer verify) throws IOException { + Map types = + Arrays.stream(FIELD_TYPES).collect(Collectors.toMap(MappedFieldType::name, Function.identity())); + CompositeAggregationBuilder aggregationBuilder = create.get(); + Sort indexSort = useIndexSort ? buildIndexSort(aggregationBuilder.sources(), types) : null; + IndexSettings indexSettings = createIndexSettings(indexSort); try (Directory directory = newDirectory()) { - try (RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory)) { + IndexWriterConfig config = newIndexWriterConfig(random(), new MockAnalyzer(random())); + if (indexSort != null) { + config.setIndexSort(indexSort); + config.setCodec(TestUtil.getDefaultCodec()); + } + try (RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory, config)) { Document document = new Document(); for (Map> fields : dataset) { addToDocument(document, fields); indexWriter.addDocument(document); document.clear(); } + if (reduced == false && randomBoolean()) { + indexWriter.forceMerge(1); + } } try (IndexReader indexReader = DirectoryReader.open(directory)) { IndexSearcher indexSearcher = new IndexSearcher(indexReader); - CompositeAggregationBuilder aggregationBuilder = create.get(); final InternalComposite composite; if (reduced) { - composite = searchAndReduce(indexSearcher, query, aggregationBuilder, FIELD_TYPES); + composite = searchAndReduce(indexSettings, indexSearcher, query, aggregationBuilder, FIELD_TYPES); } else { - composite = search(indexSearcher, query, aggregationBuilder, FIELD_TYPES); + composite = search(indexSettings, indexSearcher, query, aggregationBuilder, FIELD_TYPES); } verify.accept(composite); } } } + private static IndexSettings createIndexSettings(Sort sort) { + Settings.Builder builder = Settings.builder(); + if (sort != null) { + String[] fields = Arrays.stream(sort.getSort()) + .map(SortField::getField) + .toArray(String[]::new); + String[] orders = Arrays.stream(sort.getSort()) + .map((o) -> o.getReverse() ? "desc" : "asc") + .toArray(String[]::new); + builder.putList("index.sort.field", fields); + builder.putList("index.sort.order", orders); + } + return IndexSettingsModule.newIndexSettings(new Index("_index", "0"), builder.build()); + } + private void addToDocument(Document doc, Map> keys) { for (Map.Entry> entry : keys.entrySet()) { final String name = entry.getKey(); @@ -1935,4 +2038,43 @@ private static Map> createDocument(Object... fields) { private static long asLong(String dateTime) { return DateFormatters.from(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.parse(dateTime)).toInstant().toEpochMilli(); } + + private static Sort buildIndexSort(List> sources, Map fieldTypes) { + List sortFields = new ArrayList<>(); + for (CompositeValuesSourceBuilder source : sources) { + MappedFieldType type = fieldTypes.get(source.field()); + if (type instanceof KeywordFieldMapper.KeywordFieldType) { + sortFields.add(new SortedSetSortField(type.name(), false)); + } else if (type instanceof DateFieldMapper.DateFieldType) { + sortFields.add(new SortedNumericSortField(type.name(), SortField.Type.LONG, false)); + } else if (type instanceof NumberFieldMapper.NumberFieldType) { + boolean comp = false; + switch (type.typeName()) { + case "byte": + case "short": + case "integer": + comp = true; + sortFields.add(new SortedNumericSortField(type.name(), SortField.Type.INT, false)); + break; + + case "long": + sortFields.add(new SortedNumericSortField(type.name(), SortField.Type.LONG, false)); + break; + + case "float": + case "double": + comp = true; + sortFields.add(new SortedNumericSortField(type.name(), SortField.Type.DOUBLE, false)); + break; + + default: + break; + } + if (comp == false) { + break; + } + } + } + return sortFields.size() > 0 ? new Sort(sortFields.toArray(new SortField[0])) : null; + } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueueTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueueTests.java index 6516309de965f..ff893314e5988 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueueTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueueTests.java @@ -29,10 +29,16 @@ import org.apache.lucene.index.DocValues; import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.search.CollectionTerminatedException; import org.apache.lucene.search.DocIdSet; import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.SortedNumericSortField; +import org.apache.lucene.search.SortedSetSortField; import org.apache.lucene.store.Directory; import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; @@ -56,6 +62,7 @@ import static org.elasticsearch.index.mapper.NumberFieldMapper.NumberType.DOUBLE; import static org.elasticsearch.index.mapper.NumberFieldMapper.NumberType.LONG; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; public class CompositeValuesCollectorQueueTests extends AggregatorTestCase { static class ClassAndName { @@ -133,31 +140,47 @@ public void testRandom() throws IOException { } private void testRandomCase(ClassAndName... types) throws IOException { - testRandomCase(true, true, types); - testRandomCase(true, false, types); - testRandomCase(false, true, types); - testRandomCase(false, false, types); + for (int i = 0; i < types.length; i++) { + testRandomCase(true, true, i, types); + testRandomCase(true, false, i, types); + testRandomCase(false, true, i, types); + testRandomCase(false, false, i, types); + } } - private void testRandomCase(boolean forceMerge, boolean missingBucket, ClassAndName... types) throws IOException { + private void testRandomCase(boolean forceMerge, + boolean missingBucket, + int indexSortSourcePrefix, + ClassAndName... types) throws IOException { final BigArrays bigArrays = BigArrays.NON_RECYCLING_INSTANCE; int numDocs = randomIntBetween(50, 100); List[]> possibleValues = new ArrayList<>(); - for (ClassAndName type : types) { + SortField[] indexSortFields = indexSortSourcePrefix == 0 ? null : new SortField[indexSortSourcePrefix]; + for (int i = 0; i < types.length; i++) { + ClassAndName type = types[i]; final Comparable[] values; int numValues = randomIntBetween(1, numDocs * 2); values = new Comparable[numValues]; if (type.clazz == Long.class) { - for (int i = 0; i < numValues; i++) { - values[i] = randomLong(); + if (i < indexSortSourcePrefix) { + indexSortFields[i] = new SortedNumericSortField(type.fieldType.name(), SortField.Type.LONG); + } + for (int j = 0; j < numValues; j++) { + values[j] = randomLong(); } } else if (type.clazz == Double.class) { - for (int i = 0; i < numValues; i++) { - values[i] = randomDouble(); + if (i < indexSortSourcePrefix) { + indexSortFields[i] = new SortedNumericSortField(type.fieldType.name(), SortField.Type.DOUBLE); + } + for (int j = 0; j < numValues; j++) { + values[j] = randomDouble(); } } else if (type.clazz == BytesRef.class) { - for (int i = 0; i < numValues; i++) { - values[i] = new BytesRef(randomAlphaOfLengthBetween(5, 50)); + if (i < indexSortSourcePrefix) { + indexSortFields[i] = new SortedSetSortField(type.fieldType.name(), false); + } + for (int j = 0; j < numValues; j++) { + values[j] = new BytesRef(randomAlphaOfLengthBetween(5, 50)); } } else { assert (false); @@ -167,13 +190,17 @@ private void testRandomCase(boolean forceMerge, boolean missingBucket, ClassAndN Set keys = new HashSet<>(); try (Directory directory = newDirectory()) { + final IndexWriterConfig writerConfig = newIndexWriterConfig(); + if (indexSortFields != null) { + writerConfig.setIndexSort(new Sort(indexSortFields)); + } try (RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory, new KeywordAnalyzer())) { for (int i = 0; i < numDocs; i++) { Document document = new Document(); List>> docValues = new ArrayList<>(); boolean hasAllField = true; for (int j = 0; j < types.length; j++) { - int numValues = randomIntBetween(0, 5); + int numValues = indexSortSourcePrefix-1 >= j ? 1 : randomIntBetween(0, 5); List> values = new ArrayList<>(); if (numValues == 0) { hasAllField = false; @@ -212,7 +239,7 @@ private void testRandomCase(boolean forceMerge, boolean missingBucket, ClassAndN } } IndexReader reader = DirectoryReader.open(directory); - int size = randomIntBetween(1, keys.size()); + int size = keys.size() > 1 ? randomIntBetween(1, keys.size()) : 1; SingleDimensionValuesSource[] sources = new SingleDimensionValuesSource[types.length]; for (int i = 0; i < types.length; i++) { final MappedFieldType fieldType = types[i].fieldType; @@ -276,21 +303,25 @@ private void testRandomCase(boolean forceMerge, boolean missingBucket, ClassAndN new CompositeValuesCollectorQueue(BigArrays.NON_RECYCLING_INSTANCE, sources, size, last); final SortedDocsProducer docsProducer = sources[0].createSortedDocsProducerOrNull(reader, new MatchAllDocsQuery()); for (LeafReaderContext leafReaderContext : reader.leaves()) { - final LeafBucketCollector leafCollector = new LeafBucketCollector() { - @Override - public void collect(int doc, long bucket) throws IOException { - queue.addIfCompetitive(); - } - }; if (docsProducer != null && withProducer) { assertEquals(DocIdSet.EMPTY, docsProducer.processLeaf(new MatchAllDocsQuery(), queue, leafReaderContext, false)); } else { + final LeafBucketCollector leafCollector = new LeafBucketCollector() { + @Override + public void collect(int doc, long bucket) throws IOException { + queue.addIfCompetitive(indexSortSourcePrefix); + } + }; final LeafBucketCollector queueCollector = queue.getLeafCollector(leafReaderContext, leafCollector); final Bits liveDocs = leafReaderContext.reader().getLiveDocs(); for (int i = 0; i < leafReaderContext.reader().maxDoc(); i++) { if (liveDocs == null || liveDocs.get(i)) { - queueCollector.collect(i); + try { + queueCollector.collect(i); + } catch (CollectionTerminatedException exc) { + assertThat(indexSortSourcePrefix, greaterThan(0)); + } } } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/InternalCompositeTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/InternalCompositeTests.java index 98263d2ebb680..4edd694a3ac03 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/InternalCompositeTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/InternalCompositeTests.java @@ -170,7 +170,7 @@ protected InternalComposite createTestInstance(String name, List o1.compareKey(o2)); CompositeKey lastBucket = buckets.size() > 0 ? buckets.get(buckets.size()-1).getRawKey() : null; - return new InternalComposite(name, size, sourceNames, formats, buckets, lastBucket, reverseMuls, + return new InternalComposite(name, size, sourceNames, formats, buckets, lastBucket, reverseMuls, randomBoolean(), Collections.emptyList(), metaData); } @@ -207,7 +207,7 @@ protected InternalComposite mutateInstance(InternalComposite instance) throws IO } CompositeKey lastBucket = buckets.size() > 0 ? buckets.get(buckets.size()-1).getRawKey() : null; return new InternalComposite(instance.getName(), instance.getSize(), sourceNames, formats, buckets, lastBucket, reverseMuls, - instance.pipelineAggregators(), metaData); + randomBoolean(), instance.pipelineAggregators(), metaData); } @Override diff --git a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java index 221030cd8d157..7c02fb2481f4c 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java @@ -306,7 +306,15 @@ protected A search(IndexSe Query query, AggregationBuilder builder, MappedFieldType... fieldTypes) throws IOException { - return search(searcher, query, builder, DEFAULT_MAX_BUCKETS, fieldTypes); + return search(createIndexSettings(), searcher, query, builder, DEFAULT_MAX_BUCKETS, fieldTypes); + } + + protected A search(IndexSettings indexSettings, + IndexSearcher searcher, + Query query, + AggregationBuilder builder, + MappedFieldType... fieldTypes) throws IOException { + return search(indexSettings, searcher, query, builder, DEFAULT_MAX_BUCKETS, fieldTypes); } protected A search(IndexSearcher searcher, @@ -314,8 +322,17 @@ protected A search(IndexSe AggregationBuilder builder, int maxBucket, MappedFieldType... fieldTypes) throws IOException { + return search(createIndexSettings(), searcher, query, builder, maxBucket, fieldTypes); + } + + protected A search(IndexSettings indexSettings, + IndexSearcher searcher, + Query query, + AggregationBuilder builder, + int maxBucket, + MappedFieldType... fieldTypes) throws IOException { MultiBucketConsumer bucketConsumer = new MultiBucketConsumer(maxBucket); - C a = createAggregator(query, builder, searcher, bucketConsumer, fieldTypes); + C a = createAggregator(query, builder, searcher, indexSettings, bucketConsumer, fieldTypes); a.preCollection(); searcher.search(query, a); a.postCollection(); @@ -329,7 +346,23 @@ protected A searchAndReduc Query query, AggregationBuilder builder, MappedFieldType... fieldTypes) throws IOException { - return searchAndReduce(searcher, query, builder, DEFAULT_MAX_BUCKETS, fieldTypes); + return searchAndReduce(createIndexSettings(), searcher, query, builder, DEFAULT_MAX_BUCKETS, fieldTypes); + } + + protected A searchAndReduce(IndexSettings indexSettings, + IndexSearcher searcher, + Query query, + AggregationBuilder builder, + MappedFieldType... fieldTypes) throws IOException { + return searchAndReduce(indexSettings, searcher, query, builder, DEFAULT_MAX_BUCKETS, fieldTypes); + } + + protected A searchAndReduce(IndexSearcher searcher, + Query query, + AggregationBuilder builder, + int maxBucket, + MappedFieldType... fieldTypes) throws IOException { + return searchAndReduce(createIndexSettings(), searcher, query, builder, maxBucket, fieldTypes); } /** @@ -337,7 +370,8 @@ protected A searchAndReduc * builds an aggregator for each sub-searcher filtered by the provided {@link Query} and * returns the reduced {@link InternalAggregation}. */ - protected A searchAndReduce(IndexSearcher searcher, + protected A searchAndReduce(IndexSettings indexSettings, + IndexSearcher searcher, Query query, AggregationBuilder builder, int maxBucket, @@ -366,7 +400,7 @@ protected A searchAndReduc for (ShardSearcher subSearcher : subSearchers) { MultiBucketConsumer shardBucketConsumer = new MultiBucketConsumer(maxBucket); - C a = createAggregator(query, builder, subSearcher, shardBucketConsumer, fieldTypes); + C a = createAggregator(query, builder, subSearcher, indexSettings, shardBucketConsumer, fieldTypes); a.preCollection(); subSearcher.search(weight, a); a.postCollection(); From d5235fffe91f7ce1f16b2a1b96934942bdfb1791 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Tue, 17 Dec 2019 14:27:06 +0100 Subject: [PATCH 234/686] Adapt BWC after backporting #50214 --- .../gateway/TransportNodesListGatewayStartedShards.java | 8 ++++---- .../store/TransportNodesListShardStoreMetaData.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java b/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java index a99ba6ab2b323..9e655fec8490d 100644 --- a/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java +++ b/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java @@ -169,7 +169,7 @@ public static class Request extends BaseNodesRequest { public Request(StreamInput in) throws IOException { super(in); shardId = new ShardId(in); - if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { customDataPath = in.readString(); } else { customDataPath = null; @@ -200,7 +200,7 @@ public String getCustomDataPath() { public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); shardId.writeTo(out); - if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { out.writeString(customDataPath); } } @@ -238,7 +238,7 @@ public static class NodeRequest extends BaseNodeRequest { public NodeRequest(StreamInput in) throws IOException { super(in); shardId = new ShardId(in); - if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { customDataPath = in.readString(); } else { customDataPath = null; @@ -254,7 +254,7 @@ public NodeRequest(Request request) { public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); shardId.writeTo(out); - if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { assert customDataPath != null; out.writeString(customDataPath); } diff --git a/server/src/main/java/org/elasticsearch/indices/store/TransportNodesListShardStoreMetaData.java b/server/src/main/java/org/elasticsearch/indices/store/TransportNodesListShardStoreMetaData.java index 1e4ef6781a761..1f00ee1d42ab1 100644 --- a/server/src/main/java/org/elasticsearch/indices/store/TransportNodesListShardStoreMetaData.java +++ b/server/src/main/java/org/elasticsearch/indices/store/TransportNodesListShardStoreMetaData.java @@ -268,7 +268,7 @@ public static class Request extends BaseNodesRequest { public Request(StreamInput in) throws IOException { super(in); shardId = new ShardId(in); - if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { customDataPath = in.readString(); } else { customDataPath = null; @@ -299,7 +299,7 @@ public String getCustomDataPath() { public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); shardId.writeTo(out); - if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { out.writeString(customDataPath); } } @@ -336,7 +336,7 @@ public static class NodeRequest extends BaseNodeRequest { public NodeRequest(StreamInput in) throws IOException { super(in); shardId = new ShardId(in); - if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { customDataPath = in.readString(); } else { customDataPath = null; @@ -352,7 +352,7 @@ public NodeRequest(Request request) { public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); shardId.writeTo(out); - if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { assert customDataPath != null; out.writeString(customDataPath); } From fecb64d0401acc302a41e807fb92264171782026 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 17 Dec 2019 14:46:43 +0100 Subject: [PATCH 235/686] Fix S3 Repo Tests Incomplete Reads (#50268) We need to read in a loop here. A single read to a huge byte array will only read 16k max with the S3 SDK so if the blob we're trying to fully read is larger we close early and fail the size comparison. Also, drain streams fully when checking existence to avoid S3 SDK warnings. --- .../repositories/blobstore/BlobStoreTestUtil.java | 6 +++++- .../blobstore/ESBlobStoreRepositoryIntegTestCase.java | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java index 75a2f70cd4f89..75f6a660fb13d 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java @@ -96,8 +96,12 @@ public static void assertRepoConsistency(InternalTestCluster testCluster, String BlobStoreTestUtil.assertConsistency(repo, repo.threadPool().executor(ThreadPool.Names.GENERIC)); } + private static final byte[] SINK = new byte[1024]; + public static boolean blobExists(BlobContainer container, String blobName) throws IOException { - try (InputStream ignored = container.readBlob(blobName)) { + try (InputStream input = container.readBlob(blobName)) { + // Drain input stream fully to avoid warnings from SDKs like S3 that don't like closing streams mid-way + while (input.read(SINK) >= 0); return true; } catch (NoSuchFileException e) { return false; diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java index f2e96603fcc98..62af8e26da073 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.repositories.IndexId; import org.elasticsearch.repositories.RepositoriesService; @@ -236,7 +237,7 @@ public static byte[] writeRandomBlob(BlobContainer container, String name, int l public static byte[] readBlobFully(BlobContainer container, String name, int length) throws IOException { byte[] data = new byte[length]; try (InputStream inputStream = container.readBlob(name)) { - assertThat(inputStream.read(data), CoreMatchers.equalTo(length)); + assertThat(Streams.readFully(inputStream, data), CoreMatchers.equalTo(length)); assertThat(inputStream.read(), CoreMatchers.equalTo(-1)); } return data; From 8e8cbc1ca76ad5ec3ef5bda6fbee77a224ee4cb2 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Tue, 17 Dec 2019 09:31:07 -0500 Subject: [PATCH 236/686] [DOCS] Add identifier mapping tip to numeric and keyword datatype docs (#49933) Users often mistakenly map numeric IDs to numeric datatypes. However, this is often slow for the `term` and other term-level queries. The "Tune for search speed" docs includes advice for mapping numeric IDs to `keyword` fields. However, this tip is not included in the `numeric` or `keyword` field datatype doc pages. This rewords the tip in the "Tune for search speed" docs, relocates it to the `numeric` field docs, and reuses it using tagged regions. --- docs/reference/how-to/search-speed.asciidoc | 8 +----- docs/reference/mapping/types/keyword.asciidoc | 10 ++++++-- docs/reference/mapping/types/numeric.asciidoc | 25 +++++++++++++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/docs/reference/how-to/search-speed.asciidoc b/docs/reference/how-to/search-speed.asciidoc index 91337b0b0a1c8..a275af5fead29 100644 --- a/docs/reference/how-to/search-speed.asciidoc +++ b/docs/reference/how-to/search-speed.asciidoc @@ -159,13 +159,7 @@ GET index/_search [[map-ids-as-keyword]] === Consider mapping identifiers as `keyword` -The fact that some data is numeric does not mean it should always be mapped as a -<>. The way that Elasticsearch indexes numbers optimizes -for `range` queries while `keyword` fields are better at `term` queries. Typically, -fields storing identifiers such as an `ISBN` or any number identifying a record -from another database are rarely used in `range` queries or aggregations. This is -why they might benefit from being mapped as <> rather than as -`integer` or `long`. +include::../mapping/types/numeric.asciidoc[tag=map-ids-as-keyword] [float] === Avoid scripts diff --git a/docs/reference/mapping/types/keyword.asciidoc b/docs/reference/mapping/types/keyword.asciidoc index e1051754c208e..71419b378faae 100644 --- a/docs/reference/mapping/types/keyword.asciidoc +++ b/docs/reference/mapping/types/keyword.asciidoc @@ -4,8 +4,8 @@ Keyword ++++ -A field to index structured content such as email addresses, hostnames, status -codes, zip codes or tags. +A field to index structured content such as IDs, email addresses, hostnames, +status codes, zip codes or tags. They are typically used for filtering (_Find me all blog posts where ++status++ is ++published++_), for sorting, and for aggregations. Keyword @@ -30,6 +30,12 @@ PUT my_index } -------------------------------- +[TIP] +.Mapping numeric identifiers +==== +include::numeric.asciidoc[tag=map-ids-as-keyword] +==== + [[keyword-params]] ==== Parameters for keyword fields diff --git a/docs/reference/mapping/types/numeric.asciidoc b/docs/reference/mapping/types/numeric.asciidoc index 72bc672b8c672..8280945b22777 100644 --- a/docs/reference/mapping/types/numeric.asciidoc +++ b/docs/reference/mapping/types/numeric.asciidoc @@ -80,6 +80,31 @@ to help make a decision. |`half_float`|+2^-24^+ |+65504+ |+11+ / +3.31+ |======================================================================= +[TIP] +.Mapping numeric identifiers +==== +// tag::map-ids-as-keyword[] +Not all numeric data should be mapped as a <> field datatype. +{es} optimizes numeric fields, such as `integer` or `long`, for +<> queries. However, <> fields +are better for <> and other +<> queries. + +Identifiers, such as an ISBN or a product ID, are rarely used in `range` +queries. However, they are often retrieved using term-level queries. + +Consider mapping a numeric identifier as a `keyword` if: + +* You don't plan to search for the identifier data using + <> queries. +* Fast retrieval is important. `term` query searches on `keyword` fields are + often faster than `term` searches on numeric fields. + +If you're unsure which to use, you can use a <> to map +the data as both a `keyword` _and_ a numeric datatype. +// end::map-ids-as-keyword[] +==== + [[number-params]] ==== Parameters for numeric fields From c46db5b908b38cda3e0d9d557268439420adebd9 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Tue, 17 Dec 2019 15:20:46 +0000 Subject: [PATCH 237/686] [ML] Delete unused data frame analytics state (#50243) This commit adds removal of unused data frame analytics state from the _delete_expired_data API (and in extend th ML daily maintenance task). At the moment the potential state docs include the progress document and state for regression and classification analyses. --- .../dataframe/DataFrameAnalyticsConfig.java | 10 +++++ .../ml/dataframe/analyses/Classification.java | 9 ++++- .../ml/dataframe/analyses/Regression.java | 9 ++++- .../DataFrameAnalyticsConfigTests.java | 8 ++++ .../analyses/ClassificationTests.java | 5 +++ .../dataframe/analyses/RegressionTests.java | 5 +++ .../ml/integration/ClassificationIT.java | 34 ++++++++++++++++ .../ml/integration/DeleteExpiredDataIT.java | 7 +--- .../MlNativeAutodetectIntegTestCase.java | 2 +- ...NativeDataFrameAnalyticsIntegTestCase.java | 4 +- .../ml/integration/MlNativeIntegTestCase.java | 11 +++++ .../xpack/ml/integration/RegressionIT.java | 34 ++++++++++++++++ ...ansportDeleteDataFrameAnalyticsAction.java | 4 +- ...sportGetDataFrameAnalyticsStatsAction.java | 2 +- .../ml/dataframe/DataFrameAnalyticsTask.java | 6 +-- .../xpack/ml/dataframe/StoredProgress.java | 14 +++++++ .../job/persistence/BatchedJobsIterator.java | 1 + .../persistence/BatchedResultsIterator.java | 1 + .../BatchedStateDocIdsIterator.java | 1 + .../job/process/normalizer/ScoresUpdater.java | 2 +- .../ml/job/retention/UnusedStateRemover.java | 32 ++++++++++++++- .../persistence/BatchedDocumentsIterator.java | 2 +- .../DocIdBatchedDocumentIterator.java | 40 +++++++++++++++++++ .../ml/dataframe/StoredProgressTests.java | 12 ++++++ .../BatchedDocumentsIteratorTests.java | 2 +- 25 files changed, 235 insertions(+), 22 deletions(-) rename x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/{job => utils}/persistence/BatchedDocumentsIterator.java (99%) create mode 100644 x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/DocIdBatchedDocumentIterator.java rename x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/{job => utils}/persistence/BatchedDocumentsIteratorTests.java (99%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java index 1142b5411fb0c..d8dcd4ee43a66 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.ml.dataframe; import org.elasticsearch.Version; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -310,6 +311,15 @@ public static String documentId(String id) { return TYPE + "-" + id; } + /** + * Returns the job id from the doc id. Returns {@code null} if the doc id is invalid. + */ + @Nullable + public static String extractJobIdFromDocId(String docId) { + String jobId = docId.replaceAll("^" + TYPE +"-", ""); + return jobId.equals(docId) ? null : jobId; + } + public static class Builder { private String id; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java index cbd78b4f3baab..f3afd387e10b5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java @@ -39,6 +39,8 @@ public class Classification implements DataFrameAnalysis { public static final ParseField TRAINING_PERCENT = new ParseField("training_percent"); public static final ParseField RANDOMIZE_SEED = new ParseField("randomize_seed"); + private static final String STATE_DOC_ID_SUFFIX = "_classification_state#1"; + private static final ConstructingObjectParser LENIENT_PARSER = createParser(true); private static final ConstructingObjectParser STRICT_PARSER = createParser(false); @@ -256,7 +258,12 @@ public boolean persistsState() { @Override public String getStateDocId(String jobId) { - return jobId + "_classification_state#1"; + return jobId + STATE_DOC_ID_SUFFIX; + } + + public static String extractJobIdFromStateDoc(String stateDocId) { + int suffixIndex = stateDocId.lastIndexOf(STATE_DOC_ID_SUFFIX); + return suffixIndex <= 0 ? null : stateDocId.substring(0, suffixIndex); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java index 8fffcd0f573da..27c8a3f2eb7ca 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java @@ -36,6 +36,8 @@ public class Regression implements DataFrameAnalysis { public static final ParseField TRAINING_PERCENT = new ParseField("training_percent"); public static final ParseField RANDOMIZE_SEED = new ParseField("randomize_seed"); + private static final String STATE_DOC_ID_SUFFIX = "_regression_state#1"; + private static final ConstructingObjectParser LENIENT_PARSER = createParser(true); private static final ConstructingObjectParser STRICT_PARSER = createParser(false); @@ -196,7 +198,12 @@ public boolean persistsState() { @Override public String getStateDocId(String jobId) { - return jobId + "_regression_state#1"; + return jobId + STATE_DOC_ID_SUFFIX; + } + + public static String extractJobIdFromStateDoc(String stateDocId) { + int suffixIndex = stateDocId.lastIndexOf(STATE_DOC_ID_SUFFIX); + return suffixIndex <= 0 ? null : stateDocId.substring(0, suffixIndex); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfigTests.java index 880bea8884658..428b63554d8cf 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfigTests.java @@ -53,6 +53,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; public class DataFrameAnalyticsConfigTests extends AbstractSerializingTestCase { @@ -384,6 +385,13 @@ public void testToXContent_GivenAnalysisWithRandomizeSeedAndVersionIsBeforeItWas } } + public void testExtractJobIdFromDocId() { + assertThat(DataFrameAnalyticsConfig.extractJobIdFromDocId("data_frame_analytics_config-foo"), equalTo("foo")); + assertThat(DataFrameAnalyticsConfig.extractJobIdFromDocId("data_frame_analytics_config-data_frame_analytics_config-foo"), + equalTo("data_frame_analytics_config-foo")); + assertThat(DataFrameAnalyticsConfig.extractJobIdFromDocId("foo"), is(nullValue())); + } + private static void assertTooSmall(ElasticsearchStatusException e) { assertThat(e.getMessage(), startsWith("model_memory_limit must be at least 1kb.")); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java index 75a7410f181ba..533839e40b524 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java @@ -216,4 +216,9 @@ public void testGetStateDocId() { String randomId = randomAlphaOfLength(10); assertThat(classification.getStateDocId(randomId), equalTo(randomId + "_classification_state#1")); } + + public void testExtractJobIdFromStateDoc() { + assertThat(Classification.extractJobIdFromStateDoc("foo_bar-1_classification_state#1"), equalTo("foo_bar-1")); + assertThat(Classification.extractJobIdFromStateDoc("noop"), is(nullValue())); + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java index 58e19f6ef6a2a..1a1dc79ad2a12 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java @@ -110,6 +110,11 @@ public void testGetStateDocId() { assertThat(regression.getStateDocId(randomId), equalTo(randomId + "_regression_state#1")); } + public void testExtractJobIdFromStateDoc() { + assertThat(Regression.extractJobIdFromStateDoc("foo_bar-1_regression_state#1"), equalTo("foo_bar-1")); + assertThat(Regression.extractJobIdFromStateDoc("noop"), is(nullValue())); + } + public void testToXContent_GivenVersionBeforeRandomizeSeedWasIntroduced() throws IOException { Regression regression = createRandom(); assertThat(regression.getRandomizeSeed(), is(notNullValue())); diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java index 0c486fdeee678..40138b826e462 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java @@ -10,6 +10,7 @@ import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.index.IndexRequest; @@ -17,6 +18,7 @@ import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.SearchHit; import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; @@ -315,6 +317,38 @@ public void testTwoJobsWithSameRandomizeSeedUseSameTrainingSet() throws Exceptio assertThat(secondRunTrainingRowsIds, equalTo(firstRunTrainingRowsIds)); } + public void testDeleteExpiredData_RemovesUnusedState() throws Exception { + initialize("classification_delete_expired_data"); + indexData(sourceIndex, 100, 0, KEYWORD_FIELD); + + DataFrameAnalyticsConfig config = buildAnalytics(jobId, sourceIndex, destIndex, null, new Classification(KEYWORD_FIELD)); + registerAnalytics(config); + putAnalytics(config); + startAnalytics(jobId); + waitUntilAnalyticsIsStopped(jobId); + + assertProgress(jobId, 100, 100, 100, 100); + assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); + assertModelStatePersisted(stateDocId()); + assertInferenceModelPersisted(jobId); + + // Call _delete_expired_data API and check nothing was deleted + assertThat(deleteExpiredData().isDeleted(), is(true)); + assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); + assertModelStatePersisted(stateDocId()); + + // Delete the config straight from the config index + DeleteResponse deleteResponse = client().prepareDelete(".ml-config", DataFrameAnalyticsConfig.documentId(jobId)) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).execute().actionGet(); + assertThat(deleteResponse.status(), equalTo(RestStatus.OK)); + + // Now calling the _delete_expired_data API should remove unused state + assertThat(deleteExpiredData().isDeleted(), is(true)); + + SearchResponse stateIndexSearchResponse = client().prepareSearch(".ml-state").execute().actionGet(); + assertThat(stateIndexSearchResponse.getHits().getTotalHits().value, equalTo(0L)); + } + private void initialize(String jobId) { this.jobId = jobId; this.sourceIndex = jobId + "_source_index"; diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DeleteExpiredDataIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DeleteExpiredDataIT.java index 26d168b5267c4..34bd7989f6a7b 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DeleteExpiredDataIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DeleteExpiredDataIT.java @@ -87,7 +87,7 @@ public void tearDownData() { cleanUp(); } - public void testDeleteExpiredDataGivenNothingToDelete() throws Exception { + public void testDeleteExpiredData_GivenNothingToDelete() throws Exception { // Tests that nothing goes wrong when there's nothing to delete client().execute(DeleteExpiredDataAction.INSTANCE, new DeleteExpiredDataAction.Request()).get(); } @@ -201,10 +201,7 @@ public void testDeleteExpiredData() throws Exception { assertThat(indexUnusedStateDocsResponse.get().status(), equalTo(RestStatus.OK)); // Now call the action under test - client().execute(DeleteExpiredDataAction.INSTANCE, new DeleteExpiredDataAction.Request()).get(); - - // We need to refresh to ensure the deletion is visible - client().admin().indices().prepareRefresh("*").get(); + assertThat(deleteExpiredData().isDeleted(), is(true)); // no-retention job should have kept all data assertThat(getBuckets("no-retention").size(), is(greaterThanOrEqualTo(70))); diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeAutodetectIntegTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeAutodetectIntegTestCase.java index fad76f8b23f80..2d41cf6409700 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeAutodetectIntegTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeAutodetectIntegTestCase.java @@ -21,6 +21,7 @@ import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; +import org.elasticsearch.xpack.core.action.util.PageParams; import org.elasticsearch.xpack.core.ml.action.CloseJobAction; import org.elasticsearch.xpack.core.ml.action.DeleteDatafeedAction; import org.elasticsearch.xpack.core.ml.action.DeleteJobAction; @@ -45,7 +46,6 @@ import org.elasticsearch.xpack.core.ml.action.StopDatafeedAction; import org.elasticsearch.xpack.core.ml.action.UpdateDatafeedAction; import org.elasticsearch.xpack.core.ml.action.UpdateJobAction; -import org.elasticsearch.xpack.core.action.util.PageParams; import org.elasticsearch.xpack.core.ml.calendars.Calendar; import org.elasticsearch.xpack.core.ml.calendars.ScheduledEvent; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java index 980f5f4da5ecb..a8c96c22c6f18 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeDataFrameAnalyticsIntegTestCase.java @@ -40,7 +40,7 @@ import org.elasticsearch.xpack.core.ml.notifications.AuditorField; import org.elasticsearch.xpack.core.ml.utils.PhaseProgress; import org.elasticsearch.xpack.core.ml.utils.QueryProvider; -import org.elasticsearch.xpack.ml.dataframe.DataFrameAnalyticsTask; +import org.elasticsearch.xpack.ml.dataframe.StoredProgress; import org.hamcrest.Matcher; import org.hamcrest.Matchers; @@ -205,7 +205,7 @@ protected void assertProgress(String id, int reindexing, int loadingData, int an } protected SearchResponse searchStoredProgress(String jobId) { - String docId = DataFrameAnalyticsTask.progressDocId(jobId); + String docId = StoredProgress.documentId(jobId); return client().prepareSearch(AnomalyDetectorsIndex.jobStateIndexPattern()) .setQuery(QueryBuilders.idsQuery().addIds(docId)) .get(); diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeIntegTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeIntegTestCase.java index b26387d0b3d72..2393ad1023549 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeIntegTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeIntegTestCase.java @@ -30,6 +30,7 @@ import org.elasticsearch.xpack.core.ml.MachineLearningField; import org.elasticsearch.xpack.core.ml.MlMetadata; import org.elasticsearch.xpack.core.ml.MlTasks; +import org.elasticsearch.xpack.core.ml.action.DeleteExpiredDataAction; import org.elasticsearch.xpack.core.ml.action.OpenJobAction; import org.elasticsearch.xpack.core.ml.action.StartDataFrameAnalyticsAction; import org.elasticsearch.xpack.core.ml.action.StartDatafeedAction; @@ -142,6 +143,16 @@ private void waitForPendingTasks() { } } + protected DeleteExpiredDataAction.Response deleteExpiredData() throws Exception { + DeleteExpiredDataAction.Response response = client().execute(DeleteExpiredDataAction.INSTANCE, + new DeleteExpiredDataAction.Request()).get(); + + // We need to refresh to ensure the deletion is visible + client().admin().indices().prepareRefresh("*").get(); + + return response; + } + @Override protected void ensureClusterStateConsistency() throws IOException { if (cluster() != null && cluster().size() > 0) { diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java index 29480d711f37f..d9ad6d1e0a32e 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RegressionIT.java @@ -7,11 +7,13 @@ import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.SearchHit; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsState; @@ -272,6 +274,38 @@ public void testTwoJobsWithSameRandomizeSeedUseSameTrainingSet() throws Exceptio assertThat(secondRunTrainingRowsIds, equalTo(firstRunTrainingRowsIds)); } + public void testDeleteExpiredData_RemovesUnusedState() throws Exception { + initialize("regression_delete_expired_data"); + indexData(sourceIndex, 100, 0); + + DataFrameAnalyticsConfig config = buildAnalytics(jobId, sourceIndex, destIndex, null, new Regression(DEPENDENT_VARIABLE_FIELD)); + registerAnalytics(config); + putAnalytics(config); + startAnalytics(jobId); + waitUntilAnalyticsIsStopped(jobId); + + assertProgress(jobId, 100, 100, 100, 100); + assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); + assertModelStatePersisted(stateDocId()); + assertInferenceModelPersisted(jobId); + + // Call _delete_expired_data API and check nothing was deleted + assertThat(deleteExpiredData().isDeleted(), is(true)); + assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); + assertModelStatePersisted(stateDocId()); + + // Delete the config straight from the config index + DeleteResponse deleteResponse = client().prepareDelete(".ml-config", DataFrameAnalyticsConfig.documentId(jobId)) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).execute().actionGet(); + assertThat(deleteResponse.status(), equalTo(RestStatus.OK)); + + // Now calling the _delete_expired_data API should remove unused state + assertThat(deleteExpiredData().isDeleted(), is(true)); + + SearchResponse stateIndexSearchResponse = client().prepareSearch(".ml-state").execute().actionGet(); + assertThat(stateIndexSearchResponse.getHits().getTotalHits().value, equalTo(0L)); + } + private void initialize(String jobId) { this.jobId = jobId; this.sourceIndex = jobId + "_source_index"; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java index 9d221f9c68f6f..d42cf9684cca9 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java @@ -43,7 +43,7 @@ import org.elasticsearch.xpack.core.ml.job.messages.Messages; import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; -import org.elasticsearch.xpack.ml.dataframe.DataFrameAnalyticsTask; +import org.elasticsearch.xpack.ml.dataframe.StoredProgress; import org.elasticsearch.xpack.ml.dataframe.persistence.DataFrameAnalyticsConfigProvider; import org.elasticsearch.xpack.ml.notifications.DataFrameAnalyticsAuditor; import org.elasticsearch.xpack.ml.process.MlMemoryTracker; @@ -165,7 +165,7 @@ private void deleteState(ParentTaskAssigningClient parentTaskClient, DataFrameAnalyticsConfig config, ActionListener listener) { List ids = new ArrayList<>(); - ids.add(DataFrameAnalyticsTask.progressDocId(config.getId())); + ids.add(StoredProgress.documentId(config.getId())); if (config.getAnalysis().persistsState()) { ids.add(config.getAnalysis().getStateDocId(config.getId())); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDataFrameAnalyticsStatsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDataFrameAnalyticsStatsAction.java index aadd6041ae642..4b35b092285e2 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDataFrameAnalyticsStatsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDataFrameAnalyticsStatsAction.java @@ -187,7 +187,7 @@ private void searchStoredProgresses(List configIds, ActionListener { GetDataFrameAnalyticsStatsAction.Response.Stats stats = statsResponse.getResponse().results().get(0); IndexRequest indexRequest = new IndexRequest(AnomalyDetectorsIndex.jobStateIndexWriteAlias()); - indexRequest.id(progressDocId(taskParams.getId())); + indexRequest.id(StoredProgress.documentId(taskParams.getId())); indexRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); try (XContentBuilder jsonBuilder = JsonXContent.contentBuilder()) { new StoredProgress(stats.getProgress()).toXContent(jsonBuilder, Payload.XContent.EMPTY_PARAMS); @@ -310,10 +310,6 @@ public static StartingState determineStartingState(String jobId, List extends BatchedDocumentsIterator> { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/BatchedStateDocIdsIterator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/BatchedStateDocIdsIterator.java index 92235570b47b5..65e8b75671151 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/BatchedStateDocIdsIterator.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/BatchedStateDocIdsIterator.java @@ -9,6 +9,7 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; +import org.elasticsearch.xpack.ml.utils.persistence.BatchedDocumentsIterator; /** * Iterates through the state doc ids diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ScoresUpdater.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ScoresUpdater.java index 47ab2364db67e..23834f6949355 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ScoresUpdater.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ScoresUpdater.java @@ -12,7 +12,7 @@ import org.elasticsearch.xpack.core.ml.job.results.Bucket; import org.elasticsearch.xpack.core.ml.job.results.Influencer; import org.elasticsearch.xpack.core.ml.job.results.Result; -import org.elasticsearch.xpack.ml.job.persistence.BatchedDocumentsIterator; +import org.elasticsearch.xpack.ml.utils.persistence.BatchedDocumentsIterator; import org.elasticsearch.xpack.ml.job.persistence.JobRenormalizedResultsPersister; import org.elasticsearch.xpack.ml.job.persistence.JobResultsProvider; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/UnusedStateRemover.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/UnusedStateRemover.java index c603502afd5d6..8a1d30382489f 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/UnusedStateRemover.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/UnusedStateRemover.java @@ -16,14 +16,19 @@ import org.elasticsearch.index.reindex.DeleteByQueryAction; import org.elasticsearch.index.reindex.DeleteByQueryRequest; import org.elasticsearch.xpack.core.ml.MlMetadata; +import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.Classification; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.Regression; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; import org.elasticsearch.xpack.core.ml.job.persistence.ElasticsearchMappings; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.CategorizerState; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.ModelState; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.Quantiles; +import org.elasticsearch.xpack.ml.dataframe.StoredProgress; import org.elasticsearch.xpack.ml.job.persistence.BatchedJobsIterator; import org.elasticsearch.xpack.ml.job.persistence.BatchedStateDocIdsIterator; +import org.elasticsearch.xpack.ml.utils.persistence.DocIdBatchedDocumentIterator; import java.util.ArrayList; import java.util.Arrays; @@ -93,6 +98,13 @@ private List findUnusedStateDocIds() { private Set getJobIds() { Set jobIds = new HashSet<>(); + jobIds.addAll(getAnamalyDetectionJobIds()); + jobIds.addAll(getDataFrameAnalyticsJobIds()); + return jobIds; + } + + private Set getAnamalyDetectionJobIds() { + Set jobIds = new HashSet<>(); // TODO Once at 8.0, we can stop searching for jobs in cluster state // and remove cluster service as a member all together. @@ -106,6 +118,18 @@ private Set getJobIds() { return jobIds; } + private Set getDataFrameAnalyticsJobIds() { + Set jobIds = new HashSet<>(); + + DocIdBatchedDocumentIterator iterator = new DocIdBatchedDocumentIterator(client, AnomalyDetectorsIndex.configIndexName(), + QueryBuilders.termQuery(DataFrameAnalyticsConfig.CONFIG_TYPE.getPreferredName(), DataFrameAnalyticsConfig.TYPE)); + while (iterator.hasNext()) { + Deque docIds = iterator.next(); + docIds.stream().map(DataFrameAnalyticsConfig::extractJobIdFromDocId).filter(Objects::nonNull).forEach(jobIds::add); + } + return jobIds; + } + private void executeDeleteUnusedStateDocs(List unusedDocIds, ActionListener listener) { LOGGER.info("Found [{}] unused state documents; attempting to delete", unusedDocIds.size()); @@ -137,7 +161,13 @@ private void executeDeleteUnusedStateDocs(List unusedDocIds, ActionListe private static class JobIdExtractor { private static List> extractors = Arrays.asList( - ModelState::extractJobId, Quantiles::extractJobId, CategorizerState::extractJobId); + ModelState::extractJobId, + Quantiles::extractJobId, + CategorizerState::extractJobId, + Classification::extractJobIdFromStateDoc, + Regression::extractJobIdFromStateDoc, + StoredProgress::extractJobIdFromDocId + ); private static String extractJobId(String docId) { String jobId; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/BatchedDocumentsIterator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/BatchedDocumentsIterator.java similarity index 99% rename from x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/BatchedDocumentsIterator.java rename to x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/BatchedDocumentsIterator.java index 4aebbd0743d28..119dcbdb42822 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/BatchedDocumentsIterator.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/BatchedDocumentsIterator.java @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.ml.job.persistence; +package org.elasticsearch.xpack.ml.utils.persistence; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/DocIdBatchedDocumentIterator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/DocIdBatchedDocumentIterator.java new file mode 100644 index 0000000000000..55b2cee2ff16d --- /dev/null +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/DocIdBatchedDocumentIterator.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.ml.utils.persistence; + +import org.elasticsearch.client.Client; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.SearchHit; + +import java.util.Objects; + +/** + * This is a document iterator that returns just the id of each matched document. + */ +public class DocIdBatchedDocumentIterator extends BatchedDocumentsIterator { + + private final QueryBuilder query; + + public DocIdBatchedDocumentIterator(Client client, String index, QueryBuilder query) { + super(client, index); + this.query = Objects.requireNonNull(query); + } + + @Override + protected QueryBuilder getQuery() { + return query; + } + + @Override + protected String map(SearchHit hit) { + return hit.getId(); + } + + @Override + protected boolean shouldFetchSource() { + return false; + } +} diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/StoredProgressTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/StoredProgressTests.java index 572ca816f81e6..0524fbd37c336 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/StoredProgressTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/StoredProgressTests.java @@ -13,6 +13,8 @@ import java.util.ArrayList; import java.util.List; +import static org.hamcrest.Matchers.equalTo; + public class StoredProgressTests extends AbstractXContentTestCase { @Override @@ -34,4 +36,14 @@ protected StoredProgress createTestInstance() { } return new StoredProgress(progress); } + + public void testDocumentId() { + assertThat(StoredProgress.documentId("foo"), equalTo("data_frame_analytics-foo-progress")); + } + + public void testExtractJobIdFromDocId() { + assertThat(StoredProgress.extractJobIdFromDocId("data_frame_analytics-foo-progress"), equalTo("foo")); + assertThat(StoredProgress.extractJobIdFromDocId("data_frame_analytics-data_frame_analytics-bar-progress-progress"), + equalTo("data_frame_analytics-bar-progress")); + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/BatchedDocumentsIteratorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/utils/persistence/BatchedDocumentsIteratorTests.java similarity index 99% rename from x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/BatchedDocumentsIteratorTests.java rename to x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/utils/persistence/BatchedDocumentsIteratorTests.java index 5b81002d9326c..381ff0612abe2 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/BatchedDocumentsIteratorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/utils/persistence/BatchedDocumentsIteratorTests.java @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.ml.job.persistence; +package org.elasticsearch.xpack.ml.utils.persistence; import org.apache.lucene.search.TotalHits; import org.elasticsearch.action.ActionFuture; From 6fafb19eee29459347aaaa0c428d96d662a1dbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Tue, 17 Dec 2019 17:30:17 +0100 Subject: [PATCH 238/686] Pass processConnectTimeout to the method that fetches C++ process' PID (#50276) --- .../process/AbstractNativeAnalyticsProcess.java | 5 +++-- .../dataframe/process/NativeAnalyticsProcess.java | 7 ++++--- .../process/NativeAnalyticsProcessFactory.java | 9 +++++---- .../NativeMemoryUsageEstimationProcess.java | 5 +++-- ...NativeMemoryUsageEstimationProcessFactory.java | 3 ++- .../autodetect/NativeAutodetectProcess.java | 6 ++++-- .../NativeAutodetectProcessFactory.java | 2 +- .../normalizer/NativeNormalizerProcess.java | 6 ++++-- .../NativeNormalizerProcessFactory.java | 2 +- .../xpack/ml/process/AbstractNativeProcess.java | 15 ++++++++------- .../autodetect/NativeAutodetectProcessTests.java | 12 +++++++----- .../ml/process/AbstractNativeProcessTests.java | 3 ++- 12 files changed, 44 insertions(+), 31 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AbstractNativeAnalyticsProcess.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AbstractNativeAnalyticsProcess.java index 4dece0cc10266..98925bce1eeba 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AbstractNativeAnalyticsProcess.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AbstractNativeAnalyticsProcess.java @@ -15,6 +15,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Path; +import java.time.Duration; import java.util.Iterator; import java.util.List; import java.util.Objects; @@ -28,10 +29,10 @@ abstract class AbstractNativeAnalyticsProcess extends AbstractNativeProc protected AbstractNativeAnalyticsProcess(String name, ConstructingObjectParser resultParser, String jobId, NativeController nativeController, InputStream logStream, OutputStream processInStream, InputStream processOutStream, OutputStream processRestoreStream, int numberOfFields, - List filesToDelete, Consumer onProcessCrash, + List filesToDelete, Consumer onProcessCrash, Duration processConnectTimeout, NamedXContentRegistry namedXContentRegistry) { super(jobId, nativeController, logStream, processInStream, processOutStream, processRestoreStream, numberOfFields, filesToDelete, - onProcessCrash); + onProcessCrash, processConnectTimeout); this.name = Objects.requireNonNull(name); this.resultsParser = new ProcessResultsParser<>(Objects.requireNonNull(resultParser), namedXContentRegistry); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcess.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcess.java index 55cf771f305d4..1900d95fa1c93 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcess.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcess.java @@ -15,6 +15,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Path; +import java.time.Duration; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -27,10 +28,10 @@ public class NativeAnalyticsProcess extends AbstractNativeAnalyticsProcess filesToDelete, Consumer onProcessCrash, AnalyticsProcessConfig config, - NamedXContentRegistry namedXContentRegistry) { + List filesToDelete, Consumer onProcessCrash, Duration processConnectTimeout, + AnalyticsProcessConfig config, NamedXContentRegistry namedXContentRegistry) { super(NAME, AnalyticsResult.PARSER, jobId, nativeController, logStream, processInStream, processOutStream, processRestoreStream, - numberOfFields, filesToDelete, onProcessCrash, namedXContentRegistry); + numberOfFields, filesToDelete, onProcessCrash, processConnectTimeout, namedXContentRegistry); this.config = Objects.requireNonNull(config); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcessFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcessFactory.java index d2ace11f553ae..5da16ecc8bfe6 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcessFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeAnalyticsProcessFactory.java @@ -82,10 +82,11 @@ public NativeAnalyticsProcess createAnalyticsProcess(DataFrameAnalyticsConfig co createNativeProcess(jobId, analyticsProcessConfig, filesToDelete, processPipes); - NativeAnalyticsProcess analyticsProcess = new NativeAnalyticsProcess(jobId, nativeController, processPipes.getLogStream().get(), - processPipes.getProcessInStream().get(), processPipes.getProcessOutStream().get(), - processPipes.getRestoreStream().orElse(null), numberOfFields, filesToDelete, onProcessCrash, analyticsProcessConfig, - namedXContentRegistry); + NativeAnalyticsProcess analyticsProcess = + new NativeAnalyticsProcess( + jobId, nativeController, processPipes.getLogStream().get(), processPipes.getProcessInStream().get(), + processPipes.getProcessOutStream().get(), processPipes.getRestoreStream().orElse(null), numberOfFields, filesToDelete, + onProcessCrash, processConnectTimeout, analyticsProcessConfig, namedXContentRegistry); try { startProcess(config, executorService, processPipes, analyticsProcess); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeMemoryUsageEstimationProcess.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeMemoryUsageEstimationProcess.java index 2d95dacd2719a..43d33066a8890 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeMemoryUsageEstimationProcess.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeMemoryUsageEstimationProcess.java @@ -13,6 +13,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Path; +import java.time.Duration; import java.util.List; import java.util.function.Consumer; @@ -23,9 +24,9 @@ public class NativeMemoryUsageEstimationProcess extends AbstractNativeAnalyticsP protected NativeMemoryUsageEstimationProcess(String jobId, NativeController nativeController, InputStream logStream, OutputStream processInStream, InputStream processOutStream, OutputStream processRestoreStream, int numberOfFields, List filesToDelete, - Consumer onProcessCrash) { + Consumer onProcessCrash, Duration processConnectTimeout) { super(NAME, MemoryUsageEstimationResult.PARSER, jobId, nativeController, logStream, processInStream, processOutStream, - processRestoreStream, numberOfFields, filesToDelete, onProcessCrash, NamedXContentRegistry.EMPTY); + processRestoreStream, numberOfFields, filesToDelete, onProcessCrash, processConnectTimeout, NamedXContentRegistry.EMPTY); } @Override diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeMemoryUsageEstimationProcessFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeMemoryUsageEstimationProcessFactory.java index 011a1b5f1d442..f1e8a81f337f0 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeMemoryUsageEstimationProcessFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/NativeMemoryUsageEstimationProcessFactory.java @@ -76,7 +76,8 @@ public NativeMemoryUsageEstimationProcess createAnalyticsProcess( null, 0, filesToDelete, - onProcessCrash); + onProcessCrash, + processConnectTimeout); try { process.start(executorService); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcess.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcess.java index 8f4d3d428e3d4..55abb78957bf2 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcess.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcess.java @@ -27,6 +27,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Path; +import java.time.Duration; import java.util.Iterator; import java.util.List; import java.util.function.Consumer; @@ -44,9 +45,10 @@ class NativeAutodetectProcess extends AbstractNativeProcess implements Autodetec NativeAutodetectProcess(String jobId, NativeController nativeController, InputStream logStream, OutputStream processInStream, InputStream processOutStream, OutputStream processRestoreStream, int numberOfFields, List filesToDelete, - ProcessResultsParser resultsParser, Consumer onProcessCrash) { + ProcessResultsParser resultsParser, Consumer onProcessCrash, + Duration processConnectTimeout) { super(jobId, nativeController, logStream, processInStream, processOutStream, processRestoreStream, numberOfFields, filesToDelete, - onProcessCrash); + onProcessCrash, processConnectTimeout); this.resultsParser = resultsParser; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessFactory.java index 2504439a220e7..6fd1224270d41 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessFactory.java @@ -91,7 +91,7 @@ public AutodetectProcess createAutodetectProcess(Job job, NativeAutodetectProcess autodetect = new NativeAutodetectProcess( job.getId(), nativeController, processPipes.getLogStream().get(), processPipes.getProcessInStream().get(), processPipes.getProcessOutStream().get(), processPipes.getRestoreStream().orElse(null), numberOfFields, - filesToDelete, resultsParser, onProcessCrash); + filesToDelete, resultsParser, onProcessCrash, processConnectTimeout); try { autodetect.start(executorService, stateProcessor, processPipes.getPersistStream().get()); return autodetect; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/NativeNormalizerProcess.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/NativeNormalizerProcess.java index 69c500f9d1637..f9e019a55a7e0 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/NativeNormalizerProcess.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/NativeNormalizerProcess.java @@ -11,6 +11,7 @@ import java.io.InputStream; import java.io.OutputStream; +import java.time.Duration; import java.util.Collections; /** @@ -21,8 +22,9 @@ class NativeNormalizerProcess extends AbstractNativeProcess implements Normalize private static final String NAME = "normalizer"; NativeNormalizerProcess(String jobId, NativeController nativeController, InputStream logStream, OutputStream processInStream, - InputStream processOutStream) { - super(jobId, nativeController, logStream, processInStream, processOutStream, null, 0, Collections.emptyList(), (ignore) -> {}); + InputStream processOutStream, Duration processConnectTimeout) { + super(jobId, nativeController, logStream, processInStream, processOutStream, null, 0, Collections.emptyList(), (ignore) -> {}, + processConnectTimeout); } @Override diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/NativeNormalizerProcessFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/NativeNormalizerProcessFactory.java index 0022a44f42fb9..75abd08a5fa89 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/NativeNormalizerProcessFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/NativeNormalizerProcessFactory.java @@ -53,7 +53,7 @@ public NormalizerProcess createNormalizerProcess(String jobId, String quantilesS createNativeProcess(jobId, quantilesState, processPipes, bucketSpan); NativeNormalizerProcess normalizerProcess = new NativeNormalizerProcess(jobId, nativeController, processPipes.getLogStream().get(), - processPipes.getProcessInStream().get(), processPipes.getProcessOutStream().get()); + processPipes.getProcessInStream().get(), processPipes.getProcessOutStream().get(), processConnectTimeout); try { normalizerProcess.start(executorService); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/AbstractNativeProcess.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/AbstractNativeProcess.java index db2dad41c6f87..450f5d1288d9c 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/AbstractNativeProcess.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/AbstractNativeProcess.java @@ -52,6 +52,7 @@ public abstract class AbstractNativeProcess implements NativeProcess { private final int numberOfFields; private final List filesToDelete; private final Consumer onProcessCrash; + private final Duration processConnectTimeout; private volatile Future logTailFuture; private volatile Future stateProcessorFuture; private volatile boolean processCloseInitiated; @@ -60,18 +61,19 @@ public abstract class AbstractNativeProcess implements NativeProcess { protected AbstractNativeProcess(String jobId, NativeController nativeController, InputStream logStream, OutputStream processInStream, InputStream processOutStream, OutputStream processRestoreStream, int numberOfFields, - List filesToDelete, Consumer onProcessCrash) { + List filesToDelete, Consumer onProcessCrash, Duration processConnectTimeout) { this.jobId = jobId; this.nativeController = nativeController; - cppLogHandler = new CppLogMessageHandler(jobId, logStream); + this.cppLogHandler = new CppLogMessageHandler(jobId, logStream); this.processInStream = processInStream != null ? new BufferedOutputStream(processInStream) : null; this.processOutStream = processOutStream; this.processRestoreStream = processRestoreStream; this.recordWriter = new LengthEncodedWriter(this.processInStream); - startTime = ZonedDateTime.now(); + this.startTime = ZonedDateTime.now(); this.numberOfFields = numberOfFields; this.filesToDelete = filesToDelete; this.onProcessCrash = Objects.requireNonNull(onProcessCrash); + this.processConnectTimeout = Objects.requireNonNull(processConnectTimeout); } public abstract String getName(); @@ -197,10 +199,9 @@ public void kill() throws IOException { LOGGER.debug("[{}] Killing {} process", jobId, getName()); processKilled = true; try { - // The PID comes via the processes log stream. We don't wait for it to arrive here, - // but if the wait times out it implies the process has only just started, in which - // case it should die very quickly when we close its input stream. - nativeController.killProcess(cppLogHandler.getPid(Duration.ZERO)); + // The PID comes via the processes log stream. We do wait here to give the process the time to start up and report its PID. + // Without the PID we cannot kill the process. + nativeController.killProcess(cppLogHandler.getPid(processConnectTimeout)); // Wait for the process to die before closing processInStream as if the process // is still alive when processInStream is closed it may start persisting state diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessTests.java index 145d80ecdfc6d..903938b82db65 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessTests.java @@ -26,6 +26,7 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Collections; @@ -63,7 +64,7 @@ public void testProcessStartTime() throws Exception { try (NativeAutodetectProcess process = new NativeAutodetectProcess("foo", mock(NativeController.class), logStream, mock(OutputStream.class), outputStream, mock(OutputStream.class), NUMBER_FIELDS, null, - new ProcessResultsParser<>(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY), mock(Consumer.class))) { + new ProcessResultsParser<>(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY), mock(Consumer.class), Duration.ZERO)) { process.start(executorService, mock(IndexingStateProcessor.class), mock(InputStream.class)); ZonedDateTime startTime = process.getProcessStartTime(); @@ -86,7 +87,7 @@ public void testWriteRecord() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); try (NativeAutodetectProcess process = new NativeAutodetectProcess("foo", mock(NativeController.class), logStream, bos, outputStream, mock(OutputStream.class), NUMBER_FIELDS, Collections.emptyList(), - new ProcessResultsParser<>(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY), mock(Consumer.class))) { + new ProcessResultsParser<>(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY), mock(Consumer.class), Duration.ZERO)) { process.start(executorService, mock(IndexingStateProcessor.class), mock(InputStream.class)); process.writeRecord(record); @@ -121,7 +122,7 @@ public void testFlush() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(AutodetectControlMsgWriter.FLUSH_SPACES_LENGTH + 1024); try (NativeAutodetectProcess process = new NativeAutodetectProcess("foo", mock(NativeController.class), logStream, bos, outputStream, mock(OutputStream.class), NUMBER_FIELDS, Collections.emptyList(), - new ProcessResultsParser<>(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY), mock(Consumer.class))) { + new ProcessResultsParser<>(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY), mock(Consumer.class), Duration.ZERO)) { process.start(executorService, mock(IndexingStateProcessor.class), mock(InputStream.class)); FlushJobParams params = FlushJobParams.builder().build(); @@ -155,7 +156,8 @@ public void testConsumeAndCloseOutputStream() throws IOException { try (NativeAutodetectProcess process = new NativeAutodetectProcess("foo", mock(NativeController.class), logStream, processInStream, processOutStream, mock(OutputStream.class), NUMBER_FIELDS, Collections.emptyList(), - new ProcessResultsParser(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY), mock(Consumer.class))) { + new ProcessResultsParser(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY), mock(Consumer.class), + Duration.ZERO)) { process.consumeAndCloseOutputStream(); assertThat(processOutStream.available(), equalTo(0)); @@ -171,7 +173,7 @@ private void testWriteMessage(CheckedConsumer writeFunc ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); try (NativeAutodetectProcess process = new NativeAutodetectProcess("foo", mock(NativeController.class), logStream, bos, outputStream, mock(OutputStream.class), NUMBER_FIELDS, Collections.emptyList(), - new ProcessResultsParser<>(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY), mock(Consumer.class))) { + new ProcessResultsParser<>(AutodetectResult.PARSER, NamedXContentRegistry.EMPTY), mock(Consumer.class), Duration.ZERO)) { process.start(executorService, mock(IndexingStateProcessor.class), mock(InputStream.class)); writeFunction.accept(process); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/process/AbstractNativeProcessTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/process/AbstractNativeProcessTests.java index 6a442ab089639..5f021a36c05df 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/process/AbstractNativeProcessTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/process/AbstractNativeProcessTests.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -142,7 +143,7 @@ public void testIsReady() throws Exception { private class TestNativeProcess extends AbstractNativeProcess { TestNativeProcess(OutputStream inputStream) { - super("foo", nativeController, logStream, inputStream, outputStream, restoreStream, 0, null, onProcessCrash); + super("foo", nativeController, logStream, inputStream, outputStream, restoreStream, 0, null, onProcessCrash, Duration.ZERO); } @Override From aa923ea4bfc0f34e0549924542738589d566fd0a Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Tue, 17 Dec 2019 09:34:47 -0700 Subject: [PATCH 239/686] Send hostname in SNI header in simple remote mode (#50247) Currently an intermediate proxy must route conncctions to the appropriate remote cluster when using simple mode. This commit offers a additional mechanism for the proxy to route the connections by including the hostname in the TLS SNI header. --- .../common/settings/ClusterSettings.java | 1 + .../common/settings/Setting.java | 4 ++ .../transport/RemoteClusterAware.java | 3 +- .../transport/RemoteConnectionStrategy.java | 2 +- .../transport/SimpleConnectionStrategy.java | 40 ++++++++++++++++--- .../transport/SniffConnectionStrategy.java | 6 +-- .../SimpleConnectionStrategyTests.java | 37 ++++++++++++++++- 7 files changed, 82 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index ef40f05a2f261..694c1e2bddf64 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -295,6 +295,7 @@ public void apply(Settings value, Settings current, Settings previous) { RemoteConnectionStrategy.REMOTE_CONNECTION_MODE, SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES, SimpleConnectionStrategy.REMOTE_SOCKET_CONNECTIONS, + SimpleConnectionStrategy.INCLUDE_SERVER_NAME, SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD, SniffConnectionStrategy.REMOTE_CLUSTERS_PROXY, SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS, diff --git a/server/src/main/java/org/elasticsearch/common/settings/Setting.java b/server/src/main/java/org/elasticsearch/common/settings/Setting.java index bbfe56ffcda97..05329882b7428 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/Setting.java +++ b/server/src/main/java/org/elasticsearch/common/settings/Setting.java @@ -1257,6 +1257,10 @@ public static Setting boolSetting(String key, Setting fallback return new Setting<>(key, fallbackSetting, b -> parseBoolean(b, key, isFiltered(properties)), properties); } + public static Setting boolSetting(String key, boolean defaultValue, Validator validator, Property... properties) { + return new Setting<>(key, Boolean.toString(defaultValue), b -> parseBoolean(b, key, isFiltered(properties)), validator, properties); + } + public static Setting boolSetting(String key, Function defaultValueFn, Property... properties) { return new Setting<>(key, defaultValueFn, b -> parseBoolean(b, key, isFiltered(properties)), properties); } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java index be1ca9a1a2c43..63a9b857f5f7d 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java @@ -111,7 +111,8 @@ public void listenForUpdates(ClusterSettings clusterSettings) { SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD, SniffConnectionStrategy.REMOTE_NODE_CONNECTIONS, SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES, - SimpleConnectionStrategy.REMOTE_SOCKET_CONNECTIONS); + SimpleConnectionStrategy.REMOTE_SOCKET_CONNECTIONS, + SimpleConnectionStrategy.INCLUDE_SERVER_NAME); clusterSettings.addAffixGroupUpdateConsumer(remoteClusterSettings, this::validateAndUpdateRemoteCluster); } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java index 3d994f35de224..addd68b1c535e 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java @@ -170,7 +170,7 @@ private static Stream getClusterAlias(Settings settings, Setting.Aff return allConcreteSettings.map(affixSetting::getNamespace); } - static InetSocketAddress parseSeedAddress(String remoteHost) { + static InetSocketAddress parseConfiguredAddress(String remoteHost) { final Tuple hostPort = parseHostPort(remoteHost); final String host = hostPort.v1(); assert hostPort.v2() != null : remoteHost; diff --git a/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java index 0250ff73e4e8c..9393e4d0cafc0 100644 --- a/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java @@ -26,6 +26,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodeRole; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -40,6 +41,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -49,6 +51,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.elasticsearch.common.settings.Setting.boolSetting; import static org.elasticsearch.common.settings.Setting.intSetting; public class SimpleConnectionStrategy extends RemoteConnectionStrategy { @@ -76,6 +79,15 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { (ns, key) -> intSetting(key, 18, 1, new StrategyValidator<>(ns, key, ConnectionStrategy.SIMPLE), Setting.Property.Dynamic, Setting.Property.NodeScope)); + /** + * Whether to include the hostname as a server_name attribute + */ + public static final Setting.AffixSetting INCLUDE_SERVER_NAME = Setting.affixKeySetting( + "cluster.remote.", + "simple.include_server_name", + (ns, key) -> boolSetting(key, false, new StrategyValidator<>(ns, key, ConnectionStrategy.SIMPLE), + Setting.Property.Dynamic, Setting.Property.NodeScope)); + static final int CHANNELS_PER_CONNECTION = 1; private static final int MAX_CONNECT_ATTEMPTS_PER_RUN = 3; @@ -84,6 +96,7 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { private final int maxNumConnections; private final AtomicLong counter = new AtomicLong(0); private final List configuredAddresses; + private final boolean includeServerName; private final List> addresses; private final AtomicReference remoteClusterName = new AtomicReference<>(); private final ConnectionProfile profile; @@ -96,21 +109,31 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { transportService, connectionManager, REMOTE_SOCKET_CONNECTIONS.getConcreteSettingForNamespace(clusterAlias).get(settings), - REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias).get(settings)); + REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias).get(settings), + INCLUDE_SERVER_NAME.getConcreteSettingForNamespace(clusterAlias).get(settings)); } SimpleConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, int maxNumConnections, List configuredAddresses) { this(clusterAlias, transportService, connectionManager, maxNumConnections, configuredAddresses, configuredAddresses.stream().map(address -> - (Supplier) () -> resolveAddress(address)).collect(Collectors.toList())); + (Supplier) () -> resolveAddress(address)).collect(Collectors.toList()), false); } SimpleConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, - int maxNumConnections, List configuredAddresses, List> addresses) { + int maxNumConnections, List configuredAddresses, boolean includeServerName) { + this(clusterAlias, transportService, connectionManager, maxNumConnections, configuredAddresses, + configuredAddresses.stream().map(address -> + (Supplier) () -> resolveAddress(address)).collect(Collectors.toList()), includeServerName); + } + + SimpleConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, + int maxNumConnections, List configuredAddresses, List> addresses, + boolean includeServerName) { super(clusterAlias, transportService, connectionManager); this.maxNumConnections = maxNumConnections; this.configuredAddresses = configuredAddresses; + this.includeServerName = includeServerName; assert addresses.isEmpty() == false : "Cannot use simple connection strategy with no configured addresses"; this.addresses = addresses; // TODO: Move into the ConnectionManager @@ -207,7 +230,14 @@ public void onFailure(Exception e) { for (int i = 0; i < remaining; ++i) { TransportAddress address = nextAddress(resolved); String id = clusterAlias + "#" + address; - DiscoveryNode node = new DiscoveryNode(id, address, Version.CURRENT.minimumCompatibilityVersion()); + Map attributes; + if (includeServerName) { + attributes = Collections.singletonMap("server_name", address.address().getHostString()); + } else { + attributes = Collections.emptyMap(); + } + DiscoveryNode node = new DiscoveryNode(id, address, attributes, DiscoveryNodeRole.BUILT_IN_ROLES, + Version.CURRENT.minimumCompatibilityVersion()); connectionManager.connectToNode(node, profile, clusterNameValidator, new ActionListener<>() { @Override @@ -243,7 +273,7 @@ private TransportAddress nextAddress(List resolvedAddresses) { } private static TransportAddress resolveAddress(String address) { - return new TransportAddress(parseSeedAddress(address)); + return new TransportAddress(parseConfiguredAddress(address)); } private boolean addressesChanged(final List oldAddresses, final List newAddresses) { diff --git a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java index ad1dc6696b57c..bb7d6202c59b7 100644 --- a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java @@ -425,11 +425,11 @@ public String toString() { private static DiscoveryNode resolveSeedNode(String clusterAlias, String address, String proxyAddress) { if (proxyAddress == null || proxyAddress.isEmpty()) { - TransportAddress transportAddress = new TransportAddress(parseSeedAddress(address)); + TransportAddress transportAddress = new TransportAddress(parseConfiguredAddress(address)); return new DiscoveryNode(clusterAlias + "#" + transportAddress.toString(), transportAddress, Version.CURRENT.minimumCompatibilityVersion()); } else { - TransportAddress transportAddress = new TransportAddress(parseSeedAddress(proxyAddress)); + TransportAddress transportAddress = new TransportAddress(parseConfiguredAddress(proxyAddress)); String hostName = address.substring(0, indexOfPortSeparator(address)); return new DiscoveryNode("", clusterAlias + "#" + address, UUIDs.randomBase64UUID(), hostName, address, transportAddress, Collections.singletonMap("server_name", hostName), DiscoveryNodeRole.BUILT_IN_ROLES, @@ -460,7 +460,7 @@ private static DiscoveryNode maybeAddProxyAddress(String proxyAddress, Discovery return node; } else { // resolve proxy address lazy here - InetSocketAddress proxyInetAddress = parseSeedAddress(proxyAddress); + InetSocketAddress proxyInetAddress = parseConfiguredAddress(proxyAddress); return new DiscoveryNode(node.getName(), node.getId(), node.getEphemeralId(), node.getHostName(), node .getHostAddress(), new TransportAddress(proxyInetAddress), node.getAttributes(), node.getRoles(), node.getVersion()); } diff --git a/server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java b/server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java index 4144cc856bd3a..53fe4a04d379c 100644 --- a/server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java +++ b/server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.Version; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.AbstractScopedSettings; @@ -35,6 +36,7 @@ import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -291,7 +293,7 @@ public void testSimpleStrategyWillResolveAddressesEachConnect() throws Exception int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, - numOfConnections, addresses(address), Collections.singletonList(addressSupplier))) { + numOfConnections, addresses(address), Collections.singletonList(addressSupplier), false)) { PlainActionFuture connectFuture = PlainActionFuture.newFuture(); strategy.connect(connectFuture); connectFuture.actionGet(); @@ -387,6 +389,39 @@ public void testModeSettingsCannotBeUsedWhenInDifferentMode() { } } + public void testServerNameAttributes() { + Settings bindSettings = Settings.builder().put(TransportSettings.BIND_HOST.getKey(), "localhost").build(); + try (MockTransportService transport1 = startTransport("node1", Version.CURRENT, bindSettings)) { + TransportAddress address1 = transport1.boundAddress().publishAddress(); + + try (MockTransportService localService = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool)) { + localService.start(); + localService.acceptIncomingRequests(); + + ArrayList addresses = new ArrayList<>(); + addresses.add("localhost:" + address1.getPort()); + + ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); + int numOfConnections = randomIntBetween(4, 8); + try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); + SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, + numOfConnections, addresses, true)) { + assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); + + PlainActionFuture connectFuture = PlainActionFuture.newFuture(); + strategy.connect(connectFuture); + connectFuture.actionGet(); + + assertTrue(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); + assertTrue(strategy.assertNoRunningConnections()); + + DiscoveryNode discoveryNode = connectionManager.getAllConnectedNodes().stream().findFirst().get(); + assertEquals("localhost", discoveryNode.getAttributes().get("server_name")); + } + } + } + } + private static List addresses(final TransportAddress... addresses) { return Arrays.stream(addresses).map(TransportAddress::toString).collect(Collectors.toList()); } From 4dd7870eb85a63b8c2035994ce76091ebaec9497 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 17 Dec 2019 09:01:31 -0800 Subject: [PATCH 240/686] [DOCS] Move transform resource definitions into APIs (#50108) --- .../bucket/composite-aggregation.asciidoc | 8 +- docs/reference/redirects.asciidoc | 15 +- docs/reference/rest-api/common-parms.asciidoc | 166 +++++++++++++++++- docs/reference/rest-api/defs.asciidoc | 2 - .../transform/apis/delete-transform.asciidoc | 7 +- .../apis/get-transform-stats.asciidoc | 27 +-- .../transform/apis/get-transform.asciidoc | 45 +++-- .../transform/apis/preview-transform.asciidoc | 77 ++++++-- .../transform/apis/put-transform.asciidoc | 92 +++++----- .../transform/apis/start-transform.asciidoc | 9 +- .../transform/apis/stop-transform.asciidoc | 27 +-- .../transform/apis/transformresource.asciidoc | 127 -------------- .../transform/apis/update-transform.asciidoc | 112 ++++++------ 13 files changed, 394 insertions(+), 320 deletions(-) delete mode 100644 docs/reference/transform/apis/transformresource.asciidoc diff --git a/docs/reference/aggregations/bucket/composite-aggregation.asciidoc b/docs/reference/aggregations/bucket/composite-aggregation.asciidoc index dd9aea6be3e90..226d9c2d7ade1 100644 --- a/docs/reference/aggregations/bucket/composite-aggregation.asciidoc +++ b/docs/reference/aggregations/bucket/composite-aggregation.asciidoc @@ -1,5 +1,5 @@ [[search-aggregations-bucket-composite-aggregation]] -=== Composite Aggregation +=== Composite aggregation A multi-bucket aggregation that creates composite buckets from different sources. @@ -105,6 +105,7 @@ The name given to each sources must be unique. There are three different types of values source: +[[_terms]] ===== Terms The `terms` value source is equivalent to a simple `terms` aggregation. @@ -157,6 +158,7 @@ GET /_search } -------------------------------------------------- +[[_histogram]] ===== Histogram The `histogram` value source can be applied on numeric values to build fixed size @@ -212,8 +214,8 @@ GET /_search } -------------------------------------------------- - -===== Date Histogram +[[_date_histogram]] +===== Date histogram The `date_histogram` is similar to the `histogram` value source except that the interval is specified by date/time expression: diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 69d7765e8a9b0..32a30a326bfde 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -898,17 +898,17 @@ See <>. [role="exclude",id="data-frame-transform-dest"] === Dest objects -See <>. +See <>. [role="exclude",id="data-frame-transform-source"] ==== Source objects -See <>. +See <>. [role="exclude",id="data-frame-transform-pivot"] ==== Pivot objects -See <>. +See <>. [role="exclude",id="configuring-monitoring"] === Configuring monitoring @@ -1087,4 +1087,11 @@ the details in <>. This page was deleted. [[ml-snapshot-stats]] -See <> and <>. \ No newline at end of file +See <> and <>. + +[role="exclude",id="transform-resource"] +=== {transform-cap} resources + +This page was deleted. +See <>, <>, <>, +<>. diff --git a/docs/reference/rest-api/common-parms.asciidoc b/docs/reference/rest-api/common-parms.asciidoc index 0c9400bbf62f7..36b9791ebfdf0 100644 --- a/docs/reference/rest-api/common-parms.asciidoc +++ b/docs/reference/rest-api/common-parms.asciidoc @@ -47,6 +47,42 @@ This parameter also applies to <> that point to a missing or closed index. end::allow-no-indices[] +tag::allow-no-match-transforms1[] +Specifies what to do when the request: ++ +-- +* Contains wildcard expressions and there are no {transforms} that match. +* Contains the `_all` string or no identifiers and there are no matches. +* Contains wildcard expressions and there are only partial matches. + +The default value is `true`, which returns an empty `transforms` array when +there are no matches and the subset of results when there are partial matches. + +If this parameter is `false`, the request returns a `404` status code when there +are no matches or only partial matches. +-- +end::allow-no-match-transforms1[] + +tag::allow-no-match-transforms2[] +Specifies what to do when the request: ++ +-- +* Contains wildcard expressions and there are no {transforms} that match. +* Contains the `_all` string or no identifiers and there are no matches. +* Contains wildcard expressions and there are only partial matches. + +The default value is `true`, which returns a successful acknowledgement message +when there are no matches. When there are only partial matches, the API stops +the appropriate {transforms}. For example, if the request contains +`test-id1*,test-id2*` as the identifiers and there are no {transforms} +that match `test-id2*`, the API nonetheless stops the {transforms} +that match `test-id1*`. + +If this parameter is `false`, the request returns a `404` status code when there +are no matches or only partial matches. +-- +end::allow-no-match-transforms2[] + tag::analyzer[] `analyzer`:: (Optional, string) Analyzer to use for the query string. @@ -86,6 +122,18 @@ tag::default_operator[] Defaults to `OR`. end::default_operator[] +tag::dest[] +The destination for the {transform}. +end::dest[] + +tag::dest-index[] +The _destination index_ for the {transform}. +end::dest-index[] + +tag::dest-pipeline[] +The unique identifier for a <>. +end::dest-pipeline[] + tag::detailed[] `detailed`:: (Optional, boolean) @@ -187,11 +235,22 @@ https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html[HTTP accept header]. Valid values include JSON, YAML, etc. end::http-format[] +tag::frequency[] +The interval between checks for changes in the source indices when the +{transform} is running continuously. Also determines the retry interval in the +event of transient failures while the {transform} is searching or indexing. The +minimum value is `1s` and the maximum is `1h`. The default value is `1m`. +end::frequency[] + tag::from[] `from`:: (Optional, integer) Starting document offset. Defaults to `0`. end::from[] +tag::from-transforms[] +Skips the specified number of {transforms}. The default value is `0`. +end::from-transforms[] + tag::generation[] Generation number, such as `0`. {es} increments this generation number for each segment written. {es} then uses this number to derive the segment name. @@ -476,6 +535,12 @@ or use a value of `-1`. -- end::parent-task-id[] +tag::payloads[] +`payloads`:: +(Optional, boolean) If `true`, the response includes term payloads. +Defaults to `true`. +end::payloads[] + tag::pipeline[] `pipeline`:: (Optional, string) ID of the pipeline to use to preprocess incoming documents. @@ -487,11 +552,52 @@ tag::path-pipeline[] used to limit the request. end::path-pipeline[] -tag::payloads[] -`payloads`:: -(Optional, boolean) If `true`, the response includes term payloads. -Defaults to `true`. -end::payloads[] +tag::pivot[] +The method for transforming the data. These objects define the pivot function +`group by` fields and the aggregation to reduce the data. +end::pivot[] + +tag::pivot-aggs[] +Defines how to aggregate the grouped data. The following composite aggregations +are supported: ++ +-- +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> + +IMPORTANT: {transforms-cap} support a subset of the functionality in +composite aggregations. See <>. + +-- +end::pivot-aggs[] + +tag::pivot-group-by[] +Defines how to group the data. More than one grouping can be defined + per pivot. The following groupings are supported: ++ +-- +* <<_terms,Terms>> +* <<_histogram,Histogram>> +* <<_date_histogram,Date histogram>> +-- +end::pivot-group-by[] + +tag::pivot-max-page-search-size[] +Defines the initial page size to use for the composite aggregation for each +checkpoint. If circuit breaker exceptions occur, the page size is dynamically +adjusted to a lower value. The minimum value is `10` and the maximum is `10,000`. +The default value is `500`. +end::pivot-max-page-search-size[] tag::positions[] `positions`:: @@ -618,6 +724,11 @@ Configuration options for the target index. See <>. end::target-index-settings[] +tag::size-transforms[] +Specifies the maximum number of {transforms} to obtain. The default value is +`100`. +end::size-transforms[] + tag::slices[] `slices`:: (Optional, integer) The number of slices this task should be divided into. @@ -647,6 +758,21 @@ tag::source_includes[] field. end::source_includes[] +tag::source-transforms[] +The source of the data for the {transform}. +end::source-transforms[] + +tag::source-index-transforms[] +The _source indices_ for the {transform}. It can be a single index, an index +pattern (for example, `"myindex*"`), or an array of indices (for example, +`["index1", "index2"]`). +end::source-index-transforms[] + +tag::source-query-transforms[] +A query clause that retrieves a subset of data from the source index. See +<>. +end::source-query-transforms[] + tag::stats[] `stats`:: (Optional, string) Specific `tag` of the request for logging and statistical @@ -659,6 +785,24 @@ tag::stored_fields[] index rather than the document `_source`. Defaults to `false`. end::stored_fields[] +tag::sync[] +Defines the properties {transforms} require to run continuously. +end::sync[] + +tag::sync-time[] +Specifies that the {transform} uses a time field to synchronize the source and +destination indices. +end::sync-time[] + +tag::sync-time-field[] +The date field that is used to identify new documents in the source. +end::sync-time-field[] + +tag::sync-time-delay[] +The time delay between the current time and the latest input data time. The +default value is `60s`. +end::sync-time-delay[] + tag::target-index[] ``:: + @@ -712,6 +856,18 @@ end::master-timeout[] end::timeoutparms[] +tag::transform-id[] +Identifier for the {transform}. This identifier can contain lowercase +alphanumeric characters (a-z and 0-9), hyphens, and underscores. It must start +and end with alphanumeric characters. +end::transform-id[] + +tag::transform-id-wildcard[] +Identifier for the {transform}. It can be a {transform} identifier or a wildcard +expression. If you do not specify one of these options, the API returns +information for all {transforms}. +end::transform-id-wildcard[] + tag::cat-v[] `v`:: (Optional, boolean) If `true`, the response includes column headings. diff --git a/docs/reference/rest-api/defs.asciidoc b/docs/reference/rest-api/defs.asciidoc index bda7415740d8e..137af880c0a9a 100644 --- a/docs/reference/rest-api/defs.asciidoc +++ b/docs/reference/rest-api/defs.asciidoc @@ -9,10 +9,8 @@ These resource definitions are used in APIs related to {ml-features} and * <> * <> * <> -* <> include::{es-repo-dir}/ml/df-analytics/apis/analysisobjects.asciidoc[] include::{xes-repo-dir}/rest-api/security/role-mapping-resources.asciidoc[] include::{es-repo-dir}/ml/anomaly-detection/apis/resultsresource.asciidoc[] -include::{es-repo-dir}/transform/apis/transformresource.asciidoc[] diff --git a/docs/reference/transform/apis/delete-transform.asciidoc b/docs/reference/transform/apis/delete-transform.asciidoc index d3d2c2b9b2714..7257d2cbe4e35 100644 --- a/docs/reference/transform/apis/delete-transform.asciidoc +++ b/docs/reference/transform/apis/delete-transform.asciidoc @@ -1,11 +1,11 @@ [role="xpack"] [testenv="basic"] [[delete-transform]] -=== Delete {transforms} API +=== Delete {transform} API [subs="attributes"] ++++ -Delete {transforms} +Delete {transform} ++++ Deletes an existing {transform}. @@ -31,7 +31,8 @@ these privileges. For more information, see <> and ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {transform}. +(Required, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=transform-id] [[delete-transform-query-parms]] ==== {api-query-parms-title} diff --git a/docs/reference/transform/apis/get-transform-stats.asciidoc b/docs/reference/transform/apis/get-transform-stats.asciidoc index 1ac82e32a2a81..2472b03885657 100644 --- a/docs/reference/transform/apis/get-transform-stats.asciidoc +++ b/docs/reference/transform/apis/get-transform-stats.asciidoc @@ -50,35 +50,24 @@ specifying `*` as the ``, or by omitting the ==== {api-path-parms-title} ``:: - (Optional, string) Identifier for the {transform}. It can be a - {transform} identifier or a wildcard expression. If you do not - specify one of these options, the API returns information for all - {transforms}. +(Optional, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=transform-id-wildcard] [[get-transform-stats-query-parms]] ==== {api-query-parms-title} `allow_no_match`:: - (Optional, boolean) Specifies what to do when the request: -+ --- -* Contains wildcard expressions and there are no {transforms} that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns an empty `transforms` array when -there are no matches and the subset of results when there are partial matches. -If this parameter is `false`, the request returns a `404` status code when there -are no matches or only partial matches. --- +(Optional, boolean) +include::{docdir}/rest-api/common-parms.asciidoc[tag=allow-no-match-transforms1] `from`:: - (Optional, integer) Skips the specified number of {transforms}. The - default value is `0`. +(Optional, integer) +include::{docdir}/rest-api/common-parms.asciidoc[tag=from-transforms] `size`:: - (Optional, integer) Specifies the maximum number of {transforms} to obtain. The default value is `100`. +(Optional, integer) +include::{docdir}/rest-api/common-parms.asciidoc[tag=size-transforms] [[get-transform-stats-response]] ==== {api-response-body-title} diff --git a/docs/reference/transform/apis/get-transform.asciidoc b/docs/reference/transform/apis/get-transform.asciidoc index 0583b4b63af5a..30b552077c089 100644 --- a/docs/reference/transform/apis/get-transform.asciidoc +++ b/docs/reference/transform/apis/get-transform.asciidoc @@ -45,41 +45,38 @@ specifying `*` as the ``, or by omitting the ``. ==== {api-path-parms-title} ``:: - (Optional, string) Identifier for the {transform}. It can be a - {transform} identifier or a wildcard expression. If you do not - specify one of these options, the API returns information for all - {transforms}. +(Optional, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=transform-id-wildcard] [[get-transform-query-parms]] ==== {api-query-parms-title} `allow_no_match`:: -(Optional, boolean) Specifies what to do when the request: -+ --- -* Contains wildcard expressions and there are no {transforms} that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns an empty `transforms` array when -there are no matches and the subset of results when there are partial matches. -If this parameter is `false`, the request returns a `404` status code when there -are no matches or only partial matches. --- +(Optional, boolean) +include::{docdir}/rest-api/common-parms.asciidoc[tag=allow-no-match-transforms1] `from`:: - (Optional, integer) Skips the specified number of {transforms}. The - default value is `0`. +(Optional, integer) +include::{docdir}/rest-api/common-parms.asciidoc[tag=from-transforms] `size`:: - (Optional, integer) Specifies the maximum number of {transforms} to obtain. The default value is `100`. +(Optional, integer) +include::{docdir}/rest-api/common-parms.asciidoc[tag=size-transforms] [[get-transform-response]] ==== {api-response-body-title} -`transforms`:: - (array) An array of {transform} resources, which are sorted by the `id` value in - ascending order. See <>. +The API returns an array of {transform} resources, which are sorted by the `id` +value in ascending order. For the full list of properties, see +<>. + +`create_time`:: +(string) The time the {transform} was created. For example, `1576094542936`. +This property is informational; you cannot change its value. + +`version`:: +(string) The version of {es} that existed on the node when the {transform} was +created. [[get-transform-response-codes]] ==== {api-response-codes-title} @@ -149,7 +146,9 @@ The API returns the following results: } } }, - "description" : "Maximum priced ecommerce data by customer_id in Asia" + "description" : "Maximum priced ecommerce data by customer_id in Asia", + "version" : "7.5.0", + "create_time" : 1576094542936 } ] } diff --git a/docs/reference/transform/apis/preview-transform.asciidoc b/docs/reference/transform/apis/preview-transform.asciidoc index 29f5359f03034..2e8e1647c17c0 100644 --- a/docs/reference/transform/apis/preview-transform.asciidoc +++ b/docs/reference/transform/apis/preview-transform.asciidoc @@ -1,11 +1,11 @@ [role="xpack"] [testenv="basic"] [[preview-transform]] -=== Preview {transforms} API +=== Preview {transform} API [subs="attributes"] ++++ -Preview {transforms} +Preview {transform} ++++ Previews a {transform}. @@ -37,24 +37,71 @@ on all the current data in the source index. [[preview-transform-request-body]] ==== {api-request-body-title} -`source`:: - (Required, object) The source configuration, which has the following - properties: + +`description`:: +(Optional, string) Free text description of the {transform}. + +`dest`:: +(Optional, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=dest] - `index`::: - (Required, string or array) The _source indices_ for the - {transform}. It can be a single index, an index pattern (for - example, `"myindex*"`), or an array of indices (for example, - `["index1", "index2"]`). +`dest`.`index`::: +(Optional, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=dest-index] + +`dest`.`pipeline`::: +(Optional, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=dest-pipeline] - `query`::: - (Optional, object) A query clause that retrieves a subset of data from the - source index. See <>. +`frequency`:: +(Optional, <>) +include::{docdir}/rest-api/common-parms.asciidoc[tag=frequency] `pivot`:: - (Required, object) Defines the pivot function `group by` fields and the - aggregation to reduce the data. See <>. +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=pivot] + +`pivot`.`aggregations` or `aggs`::: +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=pivot-aggs] + +`pivot`.`group_by`::: +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=pivot-group-by] + +`pivot`.`max_page_search_size`::: +(Optional, integer) +include::{docdir}/rest-api/common-parms.asciidoc[tag=pivot-max-page-search-size] + +`source`:: +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=source-transforms] + +`source`.`index`::: +(Required, string or array) +include::{docdir}/rest-api/common-parms.asciidoc[tag=source-index-transforms] + +`source`.`query`::: +(Optional, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=source-query-transforms] + +`sync`:: +(Optional, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync] +`sync`.`time`::: +(Optional, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync-time] + +`sync`.`time`.`delay`:::: +(Optional, <>) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync-time-delay] + +`sync`.`time`.`field`:::: +(Optional, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync-time-field] + + [[preview-transform-response]] ==== {api-response-body-title} diff --git a/docs/reference/transform/apis/put-transform.asciidoc b/docs/reference/transform/apis/put-transform.asciidoc index d40b576ea4cee..dfe11c96f1d93 100644 --- a/docs/reference/transform/apis/put-transform.asciidoc +++ b/docs/reference/transform/apis/put-transform.asciidoc @@ -1,11 +1,11 @@ [role="xpack"] [testenv="basic"] [[put-transform]] -=== Create {transforms} API +=== Create {transform} API [subs="attributes"] ++++ -Create {transforms} +Create {transform} ++++ Instantiates a {transform}. @@ -63,9 +63,8 @@ IMPORTANT: You must use {kib} or this API to create a {transform}. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {transform}. This identifier - can contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and - underscores. It must start and end with alphanumeric characters. +(Required, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=transform-id] [[put-transform-query-parms]] ==== {api-query-parms-title} @@ -82,48 +81,64 @@ IMPORTANT: You must use {kib} or this API to create a {transform}. (Optional, string) Free text description of the {transform}. `dest`:: - (Required, object) Required. The destination configuration, which has the - following properties: +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=dest] - `index`::: - (Required, string) The _destination index_ for the {transform}. +`dest`.`index`::: +(Required, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=dest-index] - `pipeline`::: - (Optional, string) The unique identifier for a <>. +`dest`.`pipeline`::: +(Optional, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=dest-pipeline] `frequency`:: - (Optional, <>) The interval between checks for changes in the source - indices when the {transform} is running continuously. Also determines - the retry interval in the event of transient failures while the {transform} is - searching or indexing. The minimum value is `1s` and the maximum is `1h`. The - default value is `1m`. +(Optional, <>) +include::{docdir}/rest-api/common-parms.asciidoc[tag=frequency] `pivot`:: - (Required, object) Defines the pivot function `group by` fields and the aggregation to - reduce the data. See <>. +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=pivot] + +`pivot`.`aggregations` or `aggs`::: +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=pivot-aggs] + +`pivot`.`group_by`::: +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=pivot-group-by] + +`pivot`.`max_page_search_size`::: +(Optional, integer) +include::{docdir}/rest-api/common-parms.asciidoc[tag=pivot-max-page-search-size] `source`:: - (Required, object) The source configuration, which has the following - properties: - - `index`::: - (Required, string or array) The _source indices_ for the - {transform}. It can be a single index, an index pattern (for - example, `"myindex*"`), or an array of indices (for example, - `["index1", "index2"]`). - - `query`::: - (Optional, object) A query clause that retrieves a subset of data from the - source index. See <>. +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=source-transforms] + +`source`.`index`::: +(Required, string or array) +include::{docdir}/rest-api/common-parms.asciidoc[tag=source-index-transforms] + +`source`.`query`::: +(Optional, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=source-query-transforms] `sync`:: - (Optional, object) Defines the properties required to run continuously. - `time`::: - (Required, object) Specifies that the {transform} uses a time - field to synchronize the source and destination indices. - `field`:::: - (Required, string) The date field that is used to identify new documents - in the source. +(Optional, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync] + +`sync`.`time`::: +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync-time] + +`sync`.`time`.`delay`:::: +(Optional, <>) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync-time-delay] + +`sync`.`time`.`field`:::: +(Required, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync-time-field] + -- TIP: In general, it’s a good idea to use a field that contains the @@ -132,9 +147,6 @@ you might need to set the `delay` such that it accounts for data transmission delays. -- - `delay`:::: - (Optional, <>) The time delay between the current time and the - latest input data time. The default value is `60s`. [[put-transform-example]] ==== {api-examples-title} diff --git a/docs/reference/transform/apis/start-transform.asciidoc b/docs/reference/transform/apis/start-transform.asciidoc index 6ce5ae7322a39..6e8ce0fdd4076 100644 --- a/docs/reference/transform/apis/start-transform.asciidoc +++ b/docs/reference/transform/apis/start-transform.asciidoc @@ -1,11 +1,11 @@ [role="xpack"] [testenv="basic"] [[start-transform]] -=== Start {transforms} API +=== Start {transform} API [subs="attributes"] ++++ -Start {transforms} +Start {transform} ++++ Starts one or more {transforms}. @@ -52,9 +52,8 @@ required privileges on the source and destination indices, the ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {transform}. This identifier - can contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and - underscores. It must start and end with alphanumeric characters. +(Required, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=transform-id] [[start-transform-example]] ==== {api-examples-title} diff --git a/docs/reference/transform/apis/stop-transform.asciidoc b/docs/reference/transform/apis/stop-transform.asciidoc index 03170d30f8728..35251dd491a40 100644 --- a/docs/reference/transform/apis/stop-transform.asciidoc +++ b/docs/reference/transform/apis/stop-transform.asciidoc @@ -45,32 +45,15 @@ All {transforms} can be stopped by using `_all` or `*` as the ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {transform}. This identifier - can contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and - underscores. It must start and end with alphanumeric characters. - +(Required, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=transform-id] [[stop-transform-query-parms]] ==== {api-query-parms-title} `allow_no_match`:: -(Optional, boolean) Specifies what to do when the request: -+ --- -* Contains wildcard expressions and there are no {transforms} that match. -* Contains the `_all` string or no identifiers and there are no matches. -* Contains wildcard expressions and there are only partial matches. - -The default value is `true`, which returns a successful acknowledgement message -when there are no matches. When there are only partial matches, the API stops -the appropriate {transforms}. For example, if the request contains -`test-id1*,test-id2*` as the identifiers and there are no {transforms} -that match `test-id2*`, the API nonetheless stops the {transforms} -that match `test-id1*`. - -If this parameter is `false`, the request returns a `404` status code when there -are no matches or only partial matches. --- +(Optional, boolean) +include::{docdir}/rest-api/common-parms.asciidoc[tag=allow-no-match-transforms2] `force`:: (Optional, boolean) Set to `true` to stop a failed {transform} or to @@ -92,7 +75,7 @@ are no matches or only partial matches. `wait_for_checkpoint`:: (Optional, boolean) If set to `true`, the transform will not completely stop - until the current checkpoint is completed. If set to `false`, the transform + until the current checkpoint is completed. If set to `false`, the {transform} stops as soon as possible. Defaults to `false`. [[stop-transform-response-codes]] diff --git a/docs/reference/transform/apis/transformresource.asciidoc b/docs/reference/transform/apis/transformresource.asciidoc deleted file mode 100644 index 190f827cd8e15..0000000000000 --- a/docs/reference/transform/apis/transformresource.asciidoc +++ /dev/null @@ -1,127 +0,0 @@ -[role="xpack"] -[testenv="basic"] -[[transform-resource]] -=== {transform-cap} resources - -{transform-cap} resources relate to the <>. - -For more information, see <>. - -[discrete] -[[transform-properties]] -==== {api-definitions-title} - -`description`:: - (string) A description of the {transform}. - -`dest`:: - (object) The destination for the {transform}. See - <>. - -`frequency`:: - (time units) The interval between checks for changes in the source indices - when the {transform} is running continuously. Also determines the - retry interval in the event of transient failures while the {transform} is - searching or indexing. The minimum value is `1s` and the maximum is `1h`. The - default value is `1m`. - -`id`:: - (string) A unique identifier for the {transform}. - -`pivot`:: - (object) The method for transforming the data. See - <>. - -`source`:: - (object) The source of the data for the {transform}. See - <>. - -[[transform-dest]] -==== Dest objects - -{transform-cap} resources contain `dest` objects. For example, when -you create a {transform}, you must define its destination. - -[discrete] -[[transform-dest-properties]] -===== {api-definitions-title} - -`index`:: - (string) The _destination index_ for the {transform}. - -`pipeline`:: - (string) The unique identifier for a <>. - -[[transform-source]] -==== Source objects - -{transform-cap} resources contain `source` objects. For example, when -you create a {transform}, you must define its source. - -[discrete] -[[transform-source-properties]] -===== {api-definitions-title} - -`index`:: - (string or array) The _source indices_ for the {transform}. It can - be a single index, an index pattern (for example, `"myindex*"`), or an array - of indices (for example, `["index1", "index2"]`). - -`query`:: - (object) A query clause that retrieves a subset of data from the source index. - See <>. - -[[transform-pivot]] -==== Pivot objects - -{transform-cap} resources contain `pivot` objects, which define the -pivot function `group by` fields and the aggregation to reduce the data. - -[discrete] -[[transform-pivot-properties]] -===== {api-definitions-title} - -`aggregations` or `aggs`:: - (object) Defines how to aggregate the grouped data. The following composite - aggregations are supported: -+ --- -* {ref}/search-aggregations-metrics-avg-aggregation.html[Average] -* {ref}/search-aggregations-metrics-weight-avg-aggregation.html[Weighted Average] -* {ref}/search-aggregations-metrics-cardinality-aggregation.html[Cardinality] -* {ref}/search-aggregations-metrics-geobounds-aggregation.html[Geo Bounds] -* {ref}/search-aggregations-metrics-geocentroid-aggregation.html[Geo Centroid] -* {ref}/search-aggregations-metrics-max-aggregation.html[Max] -* {ref}/search-aggregations-metrics-min-aggregation.html[Min] -* {ref}/search-aggregations-metrics-scripted-metric-aggregation.html[Scripted Metric] -* {ref}/search-aggregations-metrics-sum-aggregation.html[Sum] -* {ref}/search-aggregations-metrics-valuecount-aggregation.html[Value Count] -* {ref}/search-aggregations-pipeline-bucket-script-aggregation.html[Bucket Script] -* {ref}/search-aggregations-pipeline-bucket-selector-aggregation.html[Bucket Selector] - -IMPORTANT: {transforms-cap} support a subset of the functionality in -composite aggregations. See <>. - --- - -`group_by`:: - (object) Defines how to group the data. More than one grouping can be defined - per pivot. The following groupings are supported: -+ --- -* {ref}/search-aggregations-bucket-composite-aggregation.html#_terms[Terms] -* {ref}/search-aggregations-bucket-composite-aggregation.html#_histogram[Histogram] -* {ref}/search-aggregations-bucket-composite-aggregation.html#_date_histogram[Date Histogram] --- - -`max_page_search_size`:: - (integer) Defines the initial page size to use for the composite aggregation - for each checkpoint. If circuit breaker exceptions occur, the page size is - dynamically adjusted to a lower value. The minimum value is `10` and the - maximum is `10,000`. The default value is `500`. - -[[transform-example]] -==== {api-examples-title} - -See the -<>. diff --git a/docs/reference/transform/apis/update-transform.asciidoc b/docs/reference/transform/apis/update-transform.asciidoc index e169be2ff94d1..c1d77d2a95001 100644 --- a/docs/reference/transform/apis/update-transform.asciidoc +++ b/docs/reference/transform/apis/update-transform.asciidoc @@ -1,14 +1,14 @@ [role="xpack"] [testenv="basic"] [[update-transform]] -=== Update {transforms} API +=== Update {transform} API [subs="attributes"] ++++ -Update {transforms} +Update {transform} ++++ -Updates an existing {transform}. +Updates certain properties of a {transform}. beta[] @@ -30,29 +30,36 @@ privileges on the destination index. For more information, see [[update-transform-desc]] ==== {api-description-title} -This API updates an existing {transform}. All settings except description do not -take effect until after the {transform} starts the next checkpoint. This is -so there is consistency with the pivoted data in each checkpoint. +This API updates an existing {transform}. The list of properties that you can +update is a subset of the list that you can define when you create a {transform}. -IMPORTANT: When {es} {security-features} are enabled, your {transform} -remembers which roles the user who updated it had at the time of update and -runs with those privileges. +When the {transform} is updated, a series of validations occur to ensure its +success. You can use the `defer_validation` parameter to skip these checks. -IMPORTANT: You must use {kib} or this API to update a {transform}. - Do not update a {transform} directly via - `.transform-internal*` indices using the Elasticsearch index API. - If {es} {security-features} are enabled, do not give users any - privileges on `.transform-internal*` indices. If you used transforms - prior 7.5, also do not give users any privileges on - `.data-frame-internal*` indices. +All updated properties except description do not take effect until after the +{transform} starts the next checkpoint. This is so there is consistency with the +pivoted data in each checkpoint. + +[IMPORTANT] +==== + +* When {es} {security-features} are enabled, your {transform} remembers which +roles the user who updated it had at the time of update and runs with those +privileges. +* You must use {kib} or this API to update a {transform}. Do not update a +{transform} directly via `.transform-internal*` indices using the {es} index API. +If {es} {security-features} are enabled, do not give users any privileges on +`.transform-internal*` indices. If you used {transforms} prior 7.5, also do not +give users any privileges on `.data-frame-internal*` indices. + +==== [[update-transform-path-parms]] ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the {transform}. This identifier - can contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and - underscores. It must start and end with alphanumeric characters. +(Required, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=transform-id] [[update-transform-query-parms]] ==== {api-query-parms-title} @@ -69,44 +76,48 @@ IMPORTANT: You must use {kib} or this API to update a {transform}. (Optional, string) Free text description of the {transform}. `dest`:: - (Optional, object) The destination configuration, which has the - following properties: +(Optional, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=dest] - `index`::: - (Required, string) The _destination index_ for the {transform}. +`dest`.`index`::: +(Required, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=dest-index] - `pipeline`::: - (Optional, string) The unique identifier for a <>. +`dest`.`pipeline`::: +(Optional, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=dest-pipeline] `frequency`:: - (Optional, <>) The interval between checks for changes - in the source indices when the {transform} is running continuously. - Also determines the retry interval in the event of transient failures while - the {transform} is searching or indexing. The minimum value is `1s` - and the maximum is `1h`. The default value is `1m`. +(Optional, <>) +include::{docdir}/rest-api/common-parms.asciidoc[tag=frequency] `source`:: - (Optional, object) The source configuration, which has the following - properties: +(Optional, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=source-transforms] - `index`::: - (Required, string or array) The _source indices_ for the - {transform}. It can be a single index, an index pattern (for - example, `"myindex*"`), or an array of indices (for example, - `["index1", "index2"]`). +`source`.`index`::: +(Required, string or array) +include::{docdir}/rest-api/common-parms.asciidoc[tag=source-index-transforms] - `query`::: - (Optional, object) A query clause that retrieves a subset of data from the - source index. See <>. +`source`.`query`::: +(Optional, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=source-query-transforms] `sync`:: - (Optional, object) Defines the properties required to run continuously. - `time`::: - (Required, object) Specifies that the {transform} uses a time - field to synchronize the source and destination indices. - `field`:::: - (Required, string) The date field that is used to identify new documents - in the source. +(Optional, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync] + +`sync`.`time`::: +(Required, object) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync-time] + +`sync`.`time`.`delay`:::: +(Optional, <>) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync-time-delay] + +`sync`.`time`.`field`:::: +(Required, string) +include::{docdir}/rest-api/common-parms.asciidoc[tag=sync-time-field] + -- TIP: In general, it’s a good idea to use a field that contains the @@ -115,9 +126,6 @@ you might need to set the `delay` such that it accounts for data transmission delays. -- - `delay`:::: - (Optional, <>) The time delay between the current - time and the latest input data time. The default value is `60s`. [[update-transform-example]] ==== {api-examples-title} @@ -196,9 +204,9 @@ When the {transform} is updated, you receive the updated configuration: "delay": "120s" } }, - "version": "8.0.0-alpha1", + "version": "7.5.0", "create_time": 1518808660505 } ---- -// TESTRESPONSE[s/"version": "8.0.0-alpha1"/"version": $body.version/] +// TESTRESPONSE[s/"version": "7.5.0"/"version": $body.version/] // TESTRESPONSE[s/"create_time": 1518808660505/"create_time": $body.create_time/] From d18176f11863d3a5ef777a9d8cdb7258a8d5a21a Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Tue, 17 Dec 2019 09:52:16 -0800 Subject: [PATCH 241/686] Ensure global buildinfo plugin is applied for distro download (#50249) This commit ensures the global info plugin is applied, which supplies the isInternal flag used to determine whether distro download looks for bwcVersions. relates #50230 --- .../org/elasticsearch/gradle/DistributionDownloadPlugin.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/DistributionDownloadPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/DistributionDownloadPlugin.java index 88cfb80a5a2d7..861d5afcaf65c 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/DistributionDownloadPlugin.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/DistributionDownloadPlugin.java @@ -23,6 +23,7 @@ import org.elasticsearch.gradle.ElasticsearchDistribution.Platform; import org.elasticsearch.gradle.ElasticsearchDistribution.Type; import org.elasticsearch.gradle.info.BuildParams; +import org.elasticsearch.gradle.info.GlobalBuildInfoPlugin; import org.gradle.api.GradleException; import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.Plugin; @@ -66,6 +67,9 @@ public class DistributionDownloadPlugin implements Plugin { @Override public void apply(Project project) { + // this is needed for isInternal + project.getRootProject().getPluginManager().apply(GlobalBuildInfoPlugin.class); + distributionsContainer = project.container(ElasticsearchDistribution.class, name -> { Configuration fileConfiguration = project.getConfigurations().create("es_distro_file_" + name); Configuration extractedConfiguration = project.getConfigurations().create("es_distro_extracted_" + name); From 922d784dd07b6fc5497d317093493133ad471d0b Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 18 Dec 2019 10:47:12 +1100 Subject: [PATCH 242/686] Do not load SSLService in plugin contructor (#49667) XPackPlugin created an SSLService within the plugin contructor. This has 2 negative consequences: 1. The service may be constructed based on a partial view of settings. Other plugins are free to add setting values via the additionalSettings() method, but this (necessarily) happens after plugins have been constructed. 2. Any exceptions thrown during the plugin construction are handled differently than exceptions thrown during "createComponents". Since SSL configurations exceptions are relatively common, it is far preferable for them to be thrown and handled as part of the createComponents flow. This commit moves the creation of the SSLService to XPackPlugin.createComponents, and alters the sequence of some other steps to accommodate this change. Relates: #44536 --- .../elasticsearch/xpack/core/XPackPlugin.java | 19 +++++++++---- .../xpack/core/ssl/SSLService.java | 8 ++++++ .../xpack/security/Security.java | 28 +++++++++++-------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java index 02fd885b3e350..9a2d5fd65eb6d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java @@ -134,10 +134,10 @@ public XPackPlugin( final Settings settings, final Path configPath) { super(settings); + // FIXME: The settings might be changed after this (e.g. from "additionalSettings" method in other plugins) + // We should only depend on the settings from the Environment object passed to createComponents this.settings = settings; - Environment env = new Environment(settings, configPath); - setSslService(new SSLService(settings, env)); setLicenseState(new XPackLicenseState(settings)); this.licensing = new Licensing(settings); @@ -154,7 +154,14 @@ protected Clock getClock() { protected void setSslService(SSLService sslService) { XPackPlugin.sslService.set(sslService); } protected void setLicenseService(LicenseService licenseService) { XPackPlugin.licenseService.set(licenseService); } protected void setLicenseState(XPackLicenseState licenseState) { XPackPlugin.licenseState.set(licenseState); } - public static SSLService getSharedSslService() { return sslService.get(); } + + public static SSLService getSharedSslService() { + final SSLService ssl = XPackPlugin.sslService.get(); + if (ssl == null) { + throw new IllegalStateException("SSL Service is not constructed yet"); + } + return ssl; + } public static LicenseService getSharedLicenseService() { return licenseService.get(); } public static XPackLicenseState getSharedLicenseState() { return licenseState.get(); } @@ -225,14 +232,16 @@ public Collection createComponents(Client client, ClusterService cluster NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry) { List components = new ArrayList<>(); + final SSLService sslService = new SSLService(environment); + setSslService(sslService); // just create the reloader as it will pull all of the loaded ssl configurations and start watching them - new SSLConfigurationReloader(environment, getSslService(), resourceWatcherService); + new SSLConfigurationReloader(environment, sslService, resourceWatcherService); setLicenseService(new LicenseService(settings, clusterService, getClock(), environment, resourceWatcherService, getLicenseState())); // It is useful to override these as they are what guice is injecting into actions - components.add(getSslService()); + components.add(sslService); components.add(getLicenseService()); components.add(getLicenseState()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java index 803f5fb856476..a788090c831d9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java @@ -122,6 +122,14 @@ public class SSLService { private final SetOnce transportSSLConfiguration = new SetOnce<>(); private final Environment env; + /** + * Create a new SSLService using the {@code Settings} from {@link Environment#settings()}. + * @see #SSLService(Settings, Environment) + */ + public SSLService(Environment environment) { + this(environment.settings(), environment); + } + /** * Create a new SSLService that parses the settings for the ssl contexts that need to be created, creates them, and then caches them * for use later diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index fe8ff8f5903b7..3b24042f47efb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -293,7 +293,7 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw private final SetOnce securityIndex = new SetOnce<>(); private final SetOnce groupFactory = new SetOnce<>(); private final SetOnce dlsBitsetCache = new SetOnce<>(); - private final List bootstrapChecks; + private final SetOnce> bootstrapChecks = new SetOnce<>(); private final List securityExtensions = new ArrayList<>(); public Security(Settings settings, final Path configPath) { @@ -301,24 +301,19 @@ public Security(Settings settings, final Path configPath) { } Security(Settings settings, final Path configPath, List extensions) { + // TODO This is wrong. Settings can change after this. We should use the settings from createComponents this.settings = settings; + // TODO this is wrong, we should only use the environment that is provided to createComponents this.env = new Environment(settings, configPath); this.enabled = XPackSettings.SECURITY_ENABLED.get(settings); if (enabled) { runStartupChecks(settings); // we load them all here otherwise we can't access secure settings since they are closed once the checks are // fetched - final List checks = new ArrayList<>(); - checks.addAll(Arrays.asList( - new ApiKeySSLBootstrapCheck(), - new TokenSSLBootstrapCheck(), - new PkiRealmBootstrapCheck(getSslService()), - new TLSLicenseBootstrapCheck())); - checks.addAll(InternalRealms.getBootstrapChecks(settings, env)); - this.bootstrapChecks = Collections.unmodifiableList(checks); + Automatons.updateConfiguration(settings); } else { - this.bootstrapChecks = Collections.emptyList(); + this.bootstrapChecks.set(Collections.emptyList()); } this.securityExtensions.addAll(extensions); @@ -358,6 +353,17 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste return Collections.singletonList(new SecurityUsageServices(null, null, null, null)); } + // We need to construct the checks here while the secure settings are still available. + // If we wait until #getBoostrapChecks the secure settings will have been cleared/closed. + final List checks = new ArrayList<>(); + checks.addAll(Arrays.asList( + new ApiKeySSLBootstrapCheck(), + new TokenSSLBootstrapCheck(), + new PkiRealmBootstrapCheck(getSslService()), + new TLSLicenseBootstrapCheck())); + checks.addAll(InternalRealms.getBootstrapChecks(settings, env)); + this.bootstrapChecks.set(Collections.unmodifiableList(checks)); + threadContext.set(threadPool.getThreadContext()); List components = new ArrayList<>(); securityContext.set(new SecurityContext(settings, threadPool.getThreadContext())); @@ -646,7 +652,7 @@ public List getSettingsFilter() { @Override public List getBootstrapChecks() { - return bootstrapChecks; + return bootstrapChecks.get(); } @Override From 8db1aced6a76f2e346943858e75c3c234c738f0f Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Tue, 17 Dec 2019 21:26:29 -0800 Subject: [PATCH 243/686] Naming change --- .../java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index ef49d537684a8..78417857a7363 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -33,7 +33,7 @@ public class ForceMergeAction implements LifecycleAction { public static final String NAME = "forcemerge"; public static final ParseField MAX_NUM_SEGMENTS_FIELD = new ParseField("max_num_segments"); - public static final ParseField CODEC = new ParseField("index.codec"); + public static final ParseField CODEC = new ParseField("index_codec"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, a -> { From eeda2c7a4c5ae6fac8cf49cbc9cf84e8e72137e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Wed, 18 Dec 2019 09:10:12 +0100 Subject: [PATCH 244/686] [DOCS] Adds GET, GET stats and DELETE inference APIs (#50224) Co-Authored-By: Lisa Cawley --- .../delete-inference-trained-model.asciidoc | 67 +++++++++ ...get-inference-trained-model-stats.asciidoc | 134 ++++++++++++++++++ .../apis/get-inference-trained-model.asciidoc | 96 +++++++++++++ .../ml/df-analytics/apis/index.asciidoc | 11 ++ docs/reference/ml/ml-shared.asciidoc | 15 ++ 5 files changed, 323 insertions(+) create mode 100644 docs/reference/ml/df-analytics/apis/delete-inference-trained-model.asciidoc create mode 100644 docs/reference/ml/df-analytics/apis/get-inference-trained-model-stats.asciidoc create mode 100644 docs/reference/ml/df-analytics/apis/get-inference-trained-model.asciidoc diff --git a/docs/reference/ml/df-analytics/apis/delete-inference-trained-model.asciidoc b/docs/reference/ml/df-analytics/apis/delete-inference-trained-model.asciidoc new file mode 100644 index 0000000000000..f2fd703dd83da --- /dev/null +++ b/docs/reference/ml/df-analytics/apis/delete-inference-trained-model.asciidoc @@ -0,0 +1,67 @@ +[role="xpack"] +[testenv="basic"] +[[delete-inference]] +=== Delete {infer} trained model API +[subs="attributes"] +++++ +Delete {infer} trained model +++++ + +Deletes an existing trained {infer} model that is currently not referenced by an +ingest pipeline. + +experimental[] + + +[[ml-delete-inference-request]] +==== {api-request-title} + +`DELETE _ml/inference/` + + +[[ml-delete-inference-prereq]] +==== {api-prereq-title} + +* You must have `machine_learning_admin` built-in role to use this API. For more +information, see <> and <>. + + +[[ml-delete-inference-path-params]] +==== {api-path-parms-title} + +``:: +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-id] + + +[[ml-delete-inference-response-codes]] +==== {api-response-codes-title} + +`409`:: + The code indicates that the trained {infer} model is referenced by an ingest + pipeline and cannot be deleted. + + +[[ml-delete-inference-example]] +==== {api-examples-title} + +The following example deletes the `regression-job-one-1574775307356` trained +model: + +[source,console] +-------------------------------------------------- +DELETE _ml/inference/regression-job-one-1574775307356 +-------------------------------------------------- +// TEST[skip:TBD] + +The API returns the following result: + + +[source,console-result] +---- +{ + "acknowledged" : true +} +---- + + diff --git a/docs/reference/ml/df-analytics/apis/get-inference-trained-model-stats.asciidoc b/docs/reference/ml/df-analytics/apis/get-inference-trained-model-stats.asciidoc new file mode 100644 index 0000000000000..a31dc071640bc --- /dev/null +++ b/docs/reference/ml/df-analytics/apis/get-inference-trained-model-stats.asciidoc @@ -0,0 +1,134 @@ +[role="xpack"] +[testenv="basic"] +[[get-inference-stats]] +=== Get {infer} trained model statistics API +[subs="attributes"] +++++ +Get {infer} trained model stats +++++ + +Retrieves usage information for trained {infer} models. + +experimental[] + + +[[ml-get-inference-stats-request]] +==== {api-request-title} + +`GET _ml/inference/_stats` + + +`GET _ml/inference/_all/_stats` + + +`GET _ml/inference//_stats` + + +`GET _ml/inference/,/_stats` + + +`GET _ml/inference/,/_stats` + + +[[ml-get-inference-stats-prereq]] +==== {api-prereq-title} + +* You must have `monitor_ml` privilege to use this API. For more information, +see <> and <>. + + +[[ml-get-inference-stats-desc]] +==== {api-description-title} + +You can get usage information for multiple trained models in a single API +request by using a comma-separated list of model IDs or a wildcard expression. + + +[[ml-get-inference-stats-path-params]] +==== {api-path-parms-title} + +``:: +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-id] + + +[[ml-get-inference-stats-query-params]] +==== {api-query-parms-title} + +`allow_no_match`:: +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-match] + +`from`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=from] + +`size`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=size] + + +[[ml-get-inference-stats-response-codes]] +==== {api-response-codes-title} + +`404` (Missing resources):: + If `allow_no_match` is `false`, this code indicates that there are no + resources that match the request or only partial matches for the request. + + +[[ml-get-inference-stats-example]] +==== {api-examples-title} + +The following example gets usage information for all the trained models: + +[source,console] +-------------------------------------------------- +GET _ml/inference/_stats +-------------------------------------------------- +// TEST[skip:TBD] + + +The API returns the following results: + +[source,console-result] +---- +{ + "count": 2, + "trained_model_stats": [ + { + "model_id": "flight-delay-prediction-1574775339910", + "pipeline_count": 0 + }, + { + "model_id": "regression-job-one-1574775307356", + "pipeline_count": 1, + "ingest": { + "total": { + "count": 178, + "time_in_millis": 8, + "current": 0, + "failed": 0 + }, + "pipelines": { + "flight-delay": { + "count": 178, + "time_in_millis": 8, + "current": 0, + "failed": 0, + "processors": [ + { + "inference": { + "type": "inference", + "stats": { + "count": 178, + "time_in_millis": 7, + "current": 0, + "failed": 0 + } + } + } + ] + } + } + } + } + ] +} +---- +// NOTCONSOLE \ No newline at end of file diff --git a/docs/reference/ml/df-analytics/apis/get-inference-trained-model.asciidoc b/docs/reference/ml/df-analytics/apis/get-inference-trained-model.asciidoc new file mode 100644 index 0000000000000..8586962c835cc --- /dev/null +++ b/docs/reference/ml/df-analytics/apis/get-inference-trained-model.asciidoc @@ -0,0 +1,96 @@ +[role="xpack"] +[testenv="basic"] +[[get-inference]] +=== Get {infer} trained model API +[subs="attributes"] +++++ +Get {infer} trained model +++++ + +Retrieves configuration information for a trained {infer} model. + +experimental[] + + +[[ml-get-inference-request]] +==== {api-request-title} + +`GET _ml/inference/` + + +`GET _ml/inference/` + + +`GET _ml/inference/_all` + + +`GET _ml/inference/,` + + +`GET _ml/inference/` + + +[[ml-get-inference-prereq]] +==== {api-prereq-title} + +* You must have `monitor_ml` privilege to use this API. For more information, +see <> and <>. + + +[[ml-get-inference-desc]] +==== {api-description-title} + +You can get information for multiple trained models in a single API request by +using a comma-separated list of model IDs or a wildcard expression. + + +[[ml-get-inference-path-params]] +==== {api-path-parms-title} + +``:: +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-id] + + +[[ml-get-inference-query-params]] +==== {api-query-parms-title} + +`allow_no_match`:: +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-match] + +`decompress_definition`:: +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=decompress-definition] + +`from`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=from] + +`include_model_definition`:: +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=include-model-definition] + +`size`:: +(Optional, integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=size] + + +[[ml-get-inference-response-codes]] +==== {api-response-codes-title} + +`400`:: + If `include_model_definition` is `true`, this code indicates that more than + one models match the ID pattern. + +`404` (Missing resources):: + If `allow_no_match` is `false`, this code indicates that there are no + resources that match the request or only partial matches for the request. + + +[[ml-get-inference-example]] +==== {api-examples-title} + +The following example gets configuration information for all the trained models: + +[source,console] +-------------------------------------------------- +GET _ml/inference/ +-------------------------------------------------- +// TEST[skip:TBD] \ No newline at end of file diff --git a/docs/reference/ml/df-analytics/apis/index.asciidoc b/docs/reference/ml/df-analytics/apis/index.asciidoc index bebd6d3aae820..cdad89f9bb4cd 100644 --- a/docs/reference/ml/df-analytics/apis/index.asciidoc +++ b/docs/reference/ml/df-analytics/apis/index.asciidoc @@ -16,12 +16,21 @@ You can use the following APIs to perform {ml} {dfanalytics} activities. For the `analysis` object resources, check <>. + +You can use the following APIs to perform {infer} operations. + +* <> +* <> +* <> + + See also <>. //CREATE include::put-dfanalytics.asciidoc[] //DELETE include::delete-dfanalytics.asciidoc[] +include::delete-inference-trained-model.asciidoc[] //EVALUATE include::evaluate-dfanalytics.asciidoc[] //ESTIMATE_MEMORY_USAGE @@ -29,6 +38,8 @@ include::explain-dfanalytics.asciidoc[] //GET include::get-dfanalytics.asciidoc[] include::get-dfanalytics-stats.asciidoc[] +include::get-inference-trained-model.asciidoc[] +include::get-inference-trained-model-stats.asciidoc[] //SET/START/STOP include::start-dfanalytics.asciidoc[] include::stop-dfanalytics.asciidoc[] diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index 5b292f24ef515..5056281642bae 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -483,6 +483,11 @@ Identifier for the {dfeed}. It can be a {dfeed} identifier or a wildcard expression. end::datafeed-id-wildcard[] +tag::decompress-definition[] +Specifies whether the included model definition should be returned as a JSON map (`true`) or +in a custom compressed format (`false`). Defaults to `true`. +end::decompress-definition[] + tag::delayed-data-check-config[] Specifies whether the {dfeed} checks for missing data and the size of the window. For example: `{"enabled": true, "check_window": "1h"}`. @@ -688,6 +693,12 @@ tag::groups[] A list of job groups. A job can belong to no groups or many. end::groups[] +tag::include-model-definition[] +Specifies if the model definition should be returned in the response. Defaults +to `false`. When `true`, only a single model must match the ID patterns +provided, otherwise a bad request is returned. +end::include-model-definition[] + tag::indices[] An array of index names. Wildcards are supported. For example: `["it_ops_metrics", "server*"]`. @@ -828,6 +839,10 @@ recommended value. -- end::mode[] +tag::model-id[] +The unique identifier of the trained {infer} model. +end::model-id[] + tag::model-memory-limit[] The approximate maximum amount of memory resources that are required for analytical processing. Once this limit is approached, data pruning becomes From d66be06fa9f9e0f024d3daa0101f4cef228c0c6a Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Wed, 18 Dec 2019 10:36:41 +0100 Subject: [PATCH 245/686] Increase timeout on FollowIndexSecurityIT.testAutoFollowPatterns (#50282) This test was causing test failures on slow CI runs. Closes #50279 --- .../java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/ccr/qa/security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java b/x-pack/plugin/ccr/qa/security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java index 9f41ae758bf94..f8ed48777dacc 100644 --- a/x-pack/plugin/ccr/qa/security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java +++ b/x-pack/plugin/ccr/qa/security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java @@ -169,7 +169,7 @@ public void testAutoFollowPatterns() throws Exception { assertBusy(() -> { ensureYellow(allowedIndex); verifyDocuments(allowedIndex, 5, "*:*"); - }); + }, 30, TimeUnit.SECONDS); assertThat(indexExists(disallowedIndex), is(false)); assertBusy(() -> { verifyCcrMonitoring(allowedIndex, allowedIndex); From afc18c91b9ceefbee76d5fa3d4bce4fe8c712963 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Wed, 18 Dec 2019 11:07:53 +0000 Subject: [PATCH 246/686] Extract a create index method that only manipulates the ClusterState (#50240) * Extract IndexCreationTask execute into applyCreateIndexRequest This is the first step in preparation for separating the index creation into a few steps that only deal with the cluster state mutation and removing the IndexCreationTask altogether. * Split applyCreateIndexRequest This breaks down the logic in applyCreateIndexRequest into multiple steps that will hopefully make the service more readable and unit testable. The service creation process now goes through a few well defined steps, namely find the templates that possibly match the new index, parse the requested and template matching mappings, process the index and template matching settings, validate the wait for active shards request and create the `IndexService`, update the mappings in the `MapperService` (which is grouped together with creating the sort order for validation purposes), validate the requested and templated matching aliases and finally update the `ClusterState` to reflect the requested changes. This also removes the `IndexCreationTask` as it was a shallow indirection and migrates the tests from `IndexCreationTaskTests` to `MetaDataCreateIndexServiceTests` (making them "real" unit tests operating on the `ClusterState` rather than mocks). * Add more unit tests. * Add IT to verify we cleanup in case of failure --- .../metadata/MetaDataCreateIndexService.java | 765 ++++++++++-------- .../admin/indices/create/CreateIndexIT.java | 27 + .../MetaDataIndexTemplateServiceTests.java | 10 +- .../metadata/IndexCreationTaskTests.java | 488 ----------- .../MetaDataCreateIndexServiceTests.java | 373 +++++++++ 5 files changed, 826 insertions(+), 837 deletions(-) delete mode 100644 server/src/test/java/org/elasticsearch/cluster/metadata/IndexCreationTaskTests.java diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java index 7a6e71a101737..6a977fdaee368 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java @@ -46,6 +46,7 @@ import org.elasticsearch.cluster.routing.ShardRoutingState; import org.elasticsearch.cluster.routing.allocation.AllocationService; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Priority; import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; @@ -71,6 +72,7 @@ import org.elasticsearch.indices.cluster.IndicesClusterStateService.AllocatedIndices.IndexRemovalReason; import org.elasticsearch.threadpool.ThreadPool; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.file.Path; import java.time.Instant; @@ -85,8 +87,10 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.IntStream; +import static java.util.stream.Collectors.toList; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_AUTO_EXPAND_REPLICAS; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_CREATION_DATE; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_INDEX_UUID; @@ -230,364 +234,414 @@ private void onlyCreateIndex(final CreateIndexClusterStateUpdateRequest request, indexScopedSettings.validate(build, true); // we do validate here - index setting must be consistent request.settings(build); clusterService.submitStateUpdateTask( - "create-index [" + request.index() + "], cause [" + request.cause() + "]", - new IndexCreationTask( - logger, - allocationService, - request, - listener, - indicesService, - aliasValidator, - xContentRegistry, - settings, - this::validate, - indexScopedSettings)); - } - - interface IndexValidator { - void validate(CreateIndexClusterStateUpdateRequest request, ClusterState state); - } - - static class IndexCreationTask extends AckedClusterStateUpdateTask { - - private final IndicesService indicesService; - private final AliasValidator aliasValidator; - private final NamedXContentRegistry xContentRegistry; - private final CreateIndexClusterStateUpdateRequest request; - private final Logger logger; - private final AllocationService allocationService; - private final Settings settings; - private final IndexValidator validator; - private final IndexScopedSettings indexScopedSettings; - - IndexCreationTask(Logger logger, AllocationService allocationService, CreateIndexClusterStateUpdateRequest request, - ActionListener listener, IndicesService indicesService, - AliasValidator aliasValidator, NamedXContentRegistry xContentRegistry, - Settings settings, IndexValidator validator, IndexScopedSettings indexScopedSettings) { - super(Priority.URGENT, request, listener); - this.request = request; - this.logger = logger; - this.allocationService = allocationService; - this.indicesService = indicesService; - this.aliasValidator = aliasValidator; - this.xContentRegistry = xContentRegistry; - this.settings = settings; - this.validator = validator; - this.indexScopedSettings = indexScopedSettings; - } - - @Override - protected ClusterStateUpdateResponse newResponse(boolean acknowledged) { - return new ClusterStateUpdateResponse(acknowledged); - } - - @Override - public ClusterState execute(ClusterState currentState) throws Exception { - logger.trace("executing IndexCreationTask for [{}] against cluster state version [{}]", request, currentState.version()); - Index createdIndex = null; - String removalExtraInfo = null; - IndexRemovalReason removalReason = IndexRemovalReason.FAILURE; - try { - validator.validate(request, currentState); - - for (Alias alias : request.aliases()) { - aliasValidator.validateAlias(alias, request.index(), currentState.metaData()); + "create-index [" + request.index() + "], cause [" + request.cause() + "]", + new AckedClusterStateUpdateTask<>(Priority.URGENT, request, listener) { + @Override + protected ClusterStateUpdateResponse newResponse(boolean acknowledged) { + return new ClusterStateUpdateResponse(acknowledged); } - // we only find a template when its an API call (a new index) - // find templates, highest order are better matching - List templates = - MetaDataIndexTemplateService.findTemplates(currentState.metaData(), request.index()); - - // add the request mapping - Map mappings = MapperService.parseMapping(xContentRegistry, request.mappings()); - - Map templatesAliases = new HashMap<>(); - - List templateNames = new ArrayList<>(); - - final Index recoverFromIndex = request.recoverFrom(); - - if (recoverFromIndex == null) { - // apply templates, merging the mappings into the request mapping if exists - for (IndexTemplateMetaData template : templates) { - templateNames.add(template.getName()); - for (ObjectObjectCursor cursor : template.mappings()) { - String mappingString = cursor.value.string(); - // Templates are wrapped with their _type names, which for pre-8x templates may not - // be _doc. For now, we unwrap them based on the _type name, and then re-wrap with - // _doc - // TODO in 9x these will all have a _type of _doc so no re-wrapping will be necessary - Map templateMapping = MapperService.parseMapping(xContentRegistry, mappingString); - assert templateMapping.size() == 1 : templateMapping; - assert cursor.key.equals(templateMapping.keySet().iterator().next()) : cursor.key + " != " + templateMapping; - templateMapping = Collections.singletonMap(MapperService.SINGLE_MAPPING_NAME, - templateMapping.values().iterator().next()); - if (mappings.isEmpty()) { - mappings = templateMapping; - } - else { - XContentHelper.mergeDefaults(mappings, templateMapping); - } - } - //handle aliases - for (ObjectObjectCursor cursor : template.aliases()) { - AliasMetaData aliasMetaData = cursor.value; - //if an alias with same name came with the create index request itself, - // ignore this one taken from the index template - if (request.aliases().contains(new Alias(aliasMetaData.alias()))) { - continue; - } - //if an alias with same name was already processed, ignore this one - if (templatesAliases.containsKey(cursor.key)) { - continue; - } - - // Allow templatesAliases to be templated by replacing a token with the - // name of the index that we are applying it to - if (aliasMetaData.alias().contains("{index}")) { - String templatedAlias = aliasMetaData.alias().replace("{index}", request.index()); - aliasMetaData = AliasMetaData.newAliasMetaData(aliasMetaData, templatedAlias); - } - - aliasValidator.validateAliasMetaData(aliasMetaData, request.index(), currentState.metaData()); - templatesAliases.put(aliasMetaData.alias(), aliasMetaData); - } - } - } - Settings.Builder indexSettingsBuilder = Settings.builder(); - if (recoverFromIndex == null) { - // apply templates, here, in reverse order, since first ones are better matching - for (int i = templates.size() - 1; i >= 0; i--) { - indexSettingsBuilder.put(templates.get(i).settings()); - } - } - // now, put the request settings, so they override templates - indexSettingsBuilder.put(request.settings()); - if (indexSettingsBuilder.get(IndexMetaData.SETTING_INDEX_VERSION_CREATED.getKey()) == null) { - final DiscoveryNodes nodes = currentState.nodes(); - final Version createdVersion = Version.min(Version.CURRENT, nodes.getSmallestNonClientNodeVersion()); - indexSettingsBuilder.put(IndexMetaData.SETTING_INDEX_VERSION_CREATED.getKey(), createdVersion); - } - if (indexSettingsBuilder.get(SETTING_NUMBER_OF_SHARDS) == null) { - indexSettingsBuilder.put(SETTING_NUMBER_OF_SHARDS, settings.getAsInt(SETTING_NUMBER_OF_SHARDS, 1)); - } - if (indexSettingsBuilder.get(SETTING_NUMBER_OF_REPLICAS) == null) { - indexSettingsBuilder.put(SETTING_NUMBER_OF_REPLICAS, settings.getAsInt(SETTING_NUMBER_OF_REPLICAS, 1)); - } - if (settings.get(SETTING_AUTO_EXPAND_REPLICAS) != null && indexSettingsBuilder.get(SETTING_AUTO_EXPAND_REPLICAS) == null) { - indexSettingsBuilder.put(SETTING_AUTO_EXPAND_REPLICAS, settings.get(SETTING_AUTO_EXPAND_REPLICAS)); + @Override + public ClusterState execute(ClusterState currentState) throws Exception { + return applyCreateIndexRequest(currentState, request); } - if (indexSettingsBuilder.get(SETTING_CREATION_DATE) == null) { - indexSettingsBuilder.put(SETTING_CREATION_DATE, Instant.now().toEpochMilli()); - } - indexSettingsBuilder.put(IndexMetaData.SETTING_INDEX_PROVIDED_NAME, request.getProvidedName()); - indexSettingsBuilder.put(SETTING_INDEX_UUID, UUIDs.randomBase64UUID()); - final IndexMetaData.Builder tmpImdBuilder = IndexMetaData.builder(request.index()); - final Settings idxSettings = indexSettingsBuilder.build(); - int numTargetShards = IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.get(idxSettings); - final int routingNumShards; - final Version indexVersionCreated = IndexMetaData.SETTING_INDEX_VERSION_CREATED.get(idxSettings); - final IndexMetaData sourceMetaData = recoverFromIndex == null ? null : - currentState.metaData().getIndexSafe(recoverFromIndex); - if (sourceMetaData == null || sourceMetaData.getNumberOfShards() == 1) { - // in this case we either have no index to recover from or - // we have a source index with 1 shard and without an explicit split factor - // or one that is valid in that case we can split into whatever and auto-generate a new factor. - if (IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.exists(idxSettings)) { - routingNumShards = IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.get(idxSettings); + @Override + public void onFailure(String source, Exception e) { + if (e instanceof ResourceAlreadyExistsException) { + logger.trace(() -> new ParameterizedMessage("[{}] failed to create", request.index()), e); } else { - routingNumShards = calculateNumRoutingShards(numTargetShards, indexVersionCreated); - } - } else { - assert IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.exists(indexSettingsBuilder.build()) == false - : "index.number_of_routing_shards should not be present on the target index on resize"; - routingNumShards = sourceMetaData.getRoutingNumShards(); - } - // remove the setting it's temporary and is only relevant once we create the index - indexSettingsBuilder.remove(IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.getKey()); - tmpImdBuilder.setRoutingNumShards(routingNumShards); - - if (recoverFromIndex != null) { - assert request.resizeType() != null; - prepareResizeIndexSettings( - currentState, - mappings.keySet(), - indexSettingsBuilder, - recoverFromIndex, - request.index(), - request.resizeType(), - request.copySettings(), - indexScopedSettings); - } - final Settings actualIndexSettings = indexSettingsBuilder.build(); - - /* - * We can not check the shard limit until we have applied templates, otherwise we do not know the actual number of shards - * that will be used to create this index. - */ - checkShardLimit(actualIndexSettings, currentState); - - tmpImdBuilder.settings(actualIndexSettings); - - if (recoverFromIndex != null) { - /* - * We need to arrange that the primary term on all the shards in the shrunken index is at least as large as - * the maximum primary term on all the shards in the source index. This ensures that we have correct - * document-level semantics regarding sequence numbers in the shrunken index. - */ - final long primaryTerm = - IntStream - .range(0, sourceMetaData.getNumberOfShards()) - .mapToLong(sourceMetaData::primaryTerm) - .max() - .getAsLong(); - for (int shardId = 0; shardId < tmpImdBuilder.numberOfShards(); shardId++) { - tmpImdBuilder.primaryTerm(shardId, primaryTerm); - } - } - // Set up everything, now locally create the index to see that things are ok, and apply - final IndexMetaData tmpImd = tmpImdBuilder.build(); - ActiveShardCount waitForActiveShards = request.waitForActiveShards(); - if (waitForActiveShards == ActiveShardCount.DEFAULT) { - waitForActiveShards = tmpImd.getWaitForActiveShards(); - } - if (waitForActiveShards.validate(tmpImd.getNumberOfReplicas()) == false) { - throw new IllegalArgumentException("invalid wait_for_active_shards[" + request.waitForActiveShards() + - "]: cannot be greater than number of shard copies [" + - (tmpImd.getNumberOfReplicas() + 1) + "]"); - } - // create the index here (on the master) to validate it can be created, as well as adding the mapping - final IndexService indexService = indicesService.createIndex(tmpImd, Collections.emptyList()); - createdIndex = indexService.index(); - // now add the mappings - - MapperService mapperService = indexService.mapperService(); - if (mappings.isEmpty() == false) { - assert mappings.size() == 1 : mappings; - try { - mapperService.merge(MapperService.SINGLE_MAPPING_NAME, mappings, MergeReason.MAPPING_UPDATE); - } catch (Exception e) { - removalExtraInfo = "failed on parsing mappings on index creation"; - throw e; + logger.debug(() -> new ParameterizedMessage("[{}] failed to create", request.index()), e); } + super.onFailure(source, e); } + }); + } - if (request.recoverFrom() == null) { - // now that the mapping is merged we can validate the index sort. - // we cannot validate for index shrinking since the mapping is empty - // at this point. The validation will take place later in the process - // (when all shards are copied in a single place). - indexService.getIndexSortSupplier().get(); - } + /** + * Handles the cluster state transition to a version that reflects the {@link CreateIndexClusterStateUpdateRequest}. + * All the requested changes are firstly validated before mutating the {@link ClusterState}. + */ + ClusterState applyCreateIndexRequest(ClusterState currentState, CreateIndexClusterStateUpdateRequest request) throws Exception { + logger.trace("executing IndexCreationTask for [{}] against cluster state version [{}]", request, currentState.version()); + Index createdIndex = null; + String removalExtraInfo = null; + IndexRemovalReason removalReason = IndexRemovalReason.FAILURE; - // the context is only used for validation so it's fine to pass fake values for the shard id and the current - // timestamp - final QueryShardContext queryShardContext = - indexService.newQueryShardContext(0, null, () -> 0L, null); + validate(request, currentState); - for (Alias alias : request.aliases()) { - if (Strings.hasLength(alias.filter())) { - aliasValidator.validateAliasFilter(alias.name(), alias.filter(), queryShardContext, xContentRegistry); - } - } - for (AliasMetaData aliasMetaData : templatesAliases.values()) { - if (aliasMetaData.filter() != null) { - aliasValidator.validateAliasFilter(aliasMetaData.alias(), aliasMetaData.filter().uncompressed(), - queryShardContext, xContentRegistry); - } - } + final Index recoverFromIndex = request.recoverFrom(); + final IndexMetaData sourceMetaData = recoverFromIndex == null ? null : currentState.metaData().getIndexSafe(recoverFromIndex); - // now, update the mappings with the actual source - Map mappingsMetaData = new HashMap<>(); - DocumentMapper mapper = mapperService.documentMapper(); - if (mapper != null) { - MappingMetaData mappingMd = new MappingMetaData(mapper); - mappingsMetaData.put(mapper.type(), mappingMd); - } + // we only find a template when its an API call (a new index) + // find templates, highest order are better matching + final List templates = sourceMetaData == null ? + Collections.unmodifiableList(MetaDataIndexTemplateService.findTemplates(currentState.metaData(), request.index())) : + List.of(); - final IndexMetaData.Builder indexMetaDataBuilder = IndexMetaData.builder(request.index()) - .settings(actualIndexSettings) - .setRoutingNumShards(routingNumShards); + final Map mappings = Collections.unmodifiableMap(parseMappings(request.mappings(), templates, xContentRegistry)); - for (int shardId = 0; shardId < tmpImd.getNumberOfShards(); shardId++) { - indexMetaDataBuilder.primaryTerm(shardId, tmpImd.primaryTerm(shardId)); - } + final Settings aggregatedIndexSettings = + aggregateIndexSettings(currentState, request, templates, mappings, sourceMetaData, settings, indexScopedSettings); + int routingNumShards = getIndexNumberOfRoutingShards(aggregatedIndexSettings, sourceMetaData); - for (MappingMetaData mappingMd : mappingsMetaData.values()) { - indexMetaDataBuilder.putMapping(mappingMd); - } + // remove the setting it's temporary and is only relevant once we create the index + final Settings.Builder settingsBuilder = Settings.builder().put(aggregatedIndexSettings); + settingsBuilder.remove(IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.getKey()); + final Settings indexSettings = settingsBuilder.build(); - for (AliasMetaData aliasMetaData : templatesAliases.values()) { - indexMetaDataBuilder.putAlias(aliasMetaData); - } - for (Alias alias : request.aliases()) { - AliasMetaData aliasMetaData = AliasMetaData.builder(alias.name()).filter(alias.filter()) - .indexRouting(alias.indexRouting()).searchRouting(alias.searchRouting()).writeIndex(alias.writeIndex()).build(); - indexMetaDataBuilder.putAlias(aliasMetaData); - } + try { + final IndexService indexService = validateActiveShardCountAndCreateIndexService(request.index(), request.waitForActiveShards(), + indexSettings, routingNumShards, indicesService); + // create the index here (on the master) to validate it can be created, as well as adding the mapping + createdIndex = indexService.index(); - indexMetaDataBuilder.state(IndexMetaData.State.OPEN); + try { + updateIndexMappingsAndBuildSortOrder(indexService, mappings, sourceMetaData); + } catch (Exception e) { + removalExtraInfo = "failed on parsing mappings on index creation"; + throw e; + } - final IndexMetaData indexMetaData; - try { - indexMetaData = indexMetaDataBuilder.build(); - } catch (Exception e) { - removalExtraInfo = "failed to build index metadata"; - throw e; + // the context is only used for validation so it's fine to pass fake values for the shard id and the current + // timestamp + final List aliases = Collections.unmodifiableList( + resolveAndValidateAliases(request.index(), request.aliases(), templates, currentState.metaData(), aliasValidator, + xContentRegistry, indexService.newQueryShardContext(0, null, () -> 0L, null)) + ); + + final IndexMetaData indexMetaData; + try { + indexMetaData = buildIndexMetaData(request.index(), aliases, indexService.mapperService()::documentMapper, indexSettings, + routingNumShards, sourceMetaData); + } catch (Exception e) { + removalExtraInfo = "failed to build index metadata"; + throw e; + } + + logger.info("[{}] creating index, cause [{}], templates {}, shards [{}]/[{}], mappings {}", + request.index(), request.cause(), templates.stream().map(IndexTemplateMetaData::getName).collect(toList()), + indexMetaData.getNumberOfShards(), indexMetaData.getNumberOfReplicas(), mappings.keySet()); + + indexService.getIndexEventListener().beforeIndexAddedToCluster(indexMetaData.getIndex(), + indexMetaData.getSettings()); + final ClusterState updatedState = clusterStateCreateIndex(currentState, request.blocks(), indexMetaData, + allocationService::reroute); + + removalExtraInfo = "cleaning up after validating index on master"; + removalReason = IndexRemovalReason.NO_LONGER_ASSIGNED; + return updatedState; + } finally { + if (createdIndex != null) { + // Index was already partially created - need to clean up + indicesService.removeIndex(createdIndex, removalReason, removalExtraInfo); + } + } + } + + /** + * Parses the provided mappings json and the inheritable mappings from the templates (if any) into a map. + * + * The template mappings are applied in the order they are encountered in the list (clients should make sure the lower index, closer + * to the head of the list, templates have the highest {@link IndexTemplateMetaData#order()}) + */ + static Map parseMappings(String mappingsJson, List templates, + NamedXContentRegistry xContentRegistry) throws Exception { + Map mappings = MapperService.parseMapping(xContentRegistry, mappingsJson); + // apply templates, merging the mappings into the request mapping if exists + for (IndexTemplateMetaData template : templates) { + for (ObjectObjectCursor cursor : template.mappings()) { + String mappingString = cursor.value.string(); + // Templates are wrapped with their _type names, which for pre-8x templates may not + // be _doc. For now, we unwrap them based on the _type name, and then re-wrap with + // _doc + // TODO in 9x these will all have a _type of _doc so no re-wrapping will be necessary + Map templateMapping = MapperService.parseMapping(xContentRegistry, mappingString); + assert templateMapping.size() == 1 : templateMapping; + assert cursor.key.equals(templateMapping.keySet().iterator().next()) : cursor.key + " != " + templateMapping; + templateMapping = Collections.singletonMap(MapperService.SINGLE_MAPPING_NAME, + templateMapping.values().iterator().next()); + if (mappings.isEmpty()) { + mappings = templateMapping; + } else { + XContentHelper.mergeDefaults(mappings, templateMapping); } + } + } + return mappings; + } - indexService.getIndexEventListener().beforeIndexAddedToCluster(indexMetaData.getIndex(), - indexMetaData.getSettings()); + /** + * Validates and creates the settings for the new index based on the explicitly configured settings via the + * {@link CreateIndexClusterStateUpdateRequest}, inherited from templates and, if recovering from another index (ie. split, shrink, + * clone), the resize settings. + * + * The template mappings are applied in the order they are encountered in the list (clients should make sure the lower index, closer + * to the head of the list, templates have the highest {@link IndexTemplateMetaData#order()}) + * + * @return the aggregated settings for the new index + */ + static Settings aggregateIndexSettings(ClusterState currentState, CreateIndexClusterStateUpdateRequest request, + List templates, Map mappings, + @Nullable IndexMetaData sourceMetaData, Settings settings, + IndexScopedSettings indexScopedSettings) { + Settings.Builder indexSettingsBuilder = Settings.builder(); + if (sourceMetaData == null) { + // apply templates, here, in reverse order, since first ones are better matching + for (int i = templates.size() - 1; i >= 0; i--) { + indexSettingsBuilder.put(templates.get(i).settings()); + } + } + // now, put the request settings, so they override templates + indexSettingsBuilder.put(request.settings()); + if (indexSettingsBuilder.get(IndexMetaData.SETTING_INDEX_VERSION_CREATED.getKey()) == null) { + final DiscoveryNodes nodes = currentState.nodes(); + final Version createdVersion = Version.min(Version.CURRENT, nodes.getSmallestNonClientNodeVersion()); + indexSettingsBuilder.put(IndexMetaData.SETTING_INDEX_VERSION_CREATED.getKey(), createdVersion); + } + if (indexSettingsBuilder.get(SETTING_NUMBER_OF_SHARDS) == null) { + indexSettingsBuilder.put(SETTING_NUMBER_OF_SHARDS, settings.getAsInt(SETTING_NUMBER_OF_SHARDS, 1)); + } + if (indexSettingsBuilder.get(SETTING_NUMBER_OF_REPLICAS) == null) { + indexSettingsBuilder.put(SETTING_NUMBER_OF_REPLICAS, settings.getAsInt(SETTING_NUMBER_OF_REPLICAS, 1)); + } + if (settings.get(SETTING_AUTO_EXPAND_REPLICAS) != null && indexSettingsBuilder.get(SETTING_AUTO_EXPAND_REPLICAS) == null) { + indexSettingsBuilder.put(SETTING_AUTO_EXPAND_REPLICAS, settings.get(SETTING_AUTO_EXPAND_REPLICAS)); + } + + if (indexSettingsBuilder.get(SETTING_CREATION_DATE) == null) { + indexSettingsBuilder.put(SETTING_CREATION_DATE, Instant.now().toEpochMilli()); + } + indexSettingsBuilder.put(IndexMetaData.SETTING_INDEX_PROVIDED_NAME, request.getProvidedName()); + indexSettingsBuilder.put(SETTING_INDEX_UUID, UUIDs.randomBase64UUID()); + + if (sourceMetaData != null) { + assert request.resizeType() != null; + prepareResizeIndexSettings( + currentState, + mappings.keySet(), + indexSettingsBuilder, + request.recoverFrom(), + request.index(), + request.resizeType(), + request.copySettings(), + indexScopedSettings); + } + + Settings indexSettings = indexSettingsBuilder.build(); + /* + * We can not check the shard limit until we have applied templates, otherwise we do not know the actual number of shards + * that will be used to create this index. + */ + MetaDataCreateIndexService.checkShardLimit(indexSettings, currentState); + return indexSettings; + } - MetaData newMetaData = MetaData.builder(currentState.metaData()) - .put(indexMetaData, false) - .build(); + /** + * Calculates the number of routing shards based on the configured value in indexSettings or if recovering from another index + * it will return the value configured for that index. + */ + static int getIndexNumberOfRoutingShards(Settings indexSettings, @Nullable IndexMetaData sourceMetaData) { + final int numTargetShards = IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.get(indexSettings); + final Version indexVersionCreated = IndexMetaData.SETTING_INDEX_VERSION_CREATED.get(indexSettings); + final int routingNumShards; + if (sourceMetaData == null || sourceMetaData.getNumberOfShards() == 1) { + // in this case we either have no index to recover from or + // we have a source index with 1 shard and without an explicit split factor + // or one that is valid in that case we can split into whatever and auto-generate a new factor. + if (IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.exists(indexSettings)) { + routingNumShards = IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.get(indexSettings); + } else { + routingNumShards = calculateNumRoutingShards(numTargetShards, indexVersionCreated); + } + } else { + assert IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.exists(indexSettings) == false + : "index.number_of_routing_shards should not be present on the target index on resize"; + routingNumShards = sourceMetaData.getRoutingNumShards(); + } + return routingNumShards; + } - logger.info("[{}] creating index, cause [{}], templates {}, shards [{}]/[{}], mappings {}", - request.index(), request.cause(), templateNames, indexMetaData.getNumberOfShards(), - indexMetaData.getNumberOfReplicas(), mappings.keySet()); + /** + * Validate and resolve the aliases explicitly set for the index, together with the ones inherited from the specified + * templates. + * + * The template mappings are applied in the order they are encountered in the list (clients should make sure the lower index, closer + * to the head of the list, templates have the highest {@link IndexTemplateMetaData#order()}) + * + * @return the list of resolved aliases, with the explicitly provided aliases occurring first (having a higher priority) followed by + * the ones inherited from the templates + */ + static List resolveAndValidateAliases(String index, Set aliases, List templates, + MetaData metaData, AliasValidator aliasValidator, + NamedXContentRegistry xContentRegistry, QueryShardContext queryShardContext) { + List resolvedAliases = new ArrayList<>(); + for (Alias alias : aliases) { + aliasValidator.validateAlias(alias, index, metaData); + if (Strings.hasLength(alias.filter())) { + aliasValidator.validateAliasFilter(alias.name(), alias.filter(), queryShardContext, xContentRegistry); + } + AliasMetaData aliasMetaData = AliasMetaData.builder(alias.name()).filter(alias.filter()) + .indexRouting(alias.indexRouting()).searchRouting(alias.searchRouting()).writeIndex(alias.writeIndex()).build(); + resolvedAliases.add(aliasMetaData); + } + + Map templatesAliases = new HashMap<>(); + for (IndexTemplateMetaData template : templates) { + // handle aliases + for (ObjectObjectCursor cursor : template.aliases()) { + AliasMetaData aliasMetaData = cursor.value; + // if an alias with same name came with the create index request itself, + // ignore this one taken from the index template + if (aliases.contains(new Alias(aliasMetaData.alias()))) { + continue; + } + // if an alias with same name was already processed, ignore this one + if (templatesAliases.containsKey(cursor.key)) { + continue; + } - ClusterBlocks.Builder blocks = ClusterBlocks.builder().blocks(currentState.blocks()); - if (!request.blocks().isEmpty()) { - for (ClusterBlock block : request.blocks()) { - blocks.addIndexBlock(request.index(), block); - } + // Allow templatesAliases to be templated by replacing a token with the + // name of the index that we are applying it to + if (aliasMetaData.alias().contains("{index}")) { + String templatedAlias = aliasMetaData.alias().replace("{index}", index); + aliasMetaData = AliasMetaData.newAliasMetaData(aliasMetaData, templatedAlias); } - blocks.updateBlocks(indexMetaData); - - ClusterState updatedState = ClusterState.builder(currentState).blocks(blocks).metaData(newMetaData).build(); - - RoutingTable.Builder routingTableBuilder = RoutingTable.builder(updatedState.routingTable()) - .addAsNew(updatedState.metaData().index(request.index())); - updatedState = allocationService.reroute( - ClusterState.builder(updatedState).routingTable(routingTableBuilder.build()).build(), - "index [" + request.index() + "] created"); - removalExtraInfo = "cleaning up after validating index on master"; - removalReason = IndexRemovalReason.NO_LONGER_ASSIGNED; - return updatedState; - } finally { - if (createdIndex != null) { - // Index was already partially created - need to clean up - indicesService.removeIndex(createdIndex, removalReason, removalExtraInfo); + + aliasValidator.validateAliasMetaData(aliasMetaData, index, metaData); + if (aliasMetaData.filter() != null) { + aliasValidator.validateAliasFilter(aliasMetaData.alias(), aliasMetaData.filter().uncompressed(), + queryShardContext, xContentRegistry); } + templatesAliases.put(aliasMetaData.alias(), aliasMetaData); + resolvedAliases.add((aliasMetaData)); } } + return resolvedAliases; + } - protected void checkShardLimit(final Settings settings, final ClusterState clusterState) { - MetaDataCreateIndexService.checkShardLimit(settings, clusterState); + /** + * Creates the index into the cluster state applying the provided blocks. The final cluster state will contain an updated routing + * table based on the live nodes. + */ + static ClusterState clusterStateCreateIndex(ClusterState currentState, Set clusterBlocks, IndexMetaData indexMetaData, + BiFunction rerouteRoutingTable) { + MetaData newMetaData = MetaData.builder(currentState.metaData()) + .put(indexMetaData, false) + .build(); + + String indexName = indexMetaData.getIndex().getName(); + ClusterBlocks.Builder blocks = createClusterBlocksBuilder(currentState, indexName, clusterBlocks); + blocks.updateBlocks(indexMetaData); + + ClusterState updatedState = ClusterState.builder(currentState).blocks(blocks).metaData(newMetaData).build(); + + RoutingTable.Builder routingTableBuilder = RoutingTable.builder(updatedState.routingTable()) + .addAsNew(updatedState.metaData().index(indexName)); + updatedState = ClusterState.builder(updatedState).routingTable(routingTableBuilder.build()).build(); + return rerouteRoutingTable.apply(updatedState, "index [" + indexName + "] created"); + } + + static IndexMetaData buildIndexMetaData(String indexName, List aliases, + Supplier documentMapperSupplier, Settings indexSettings, int routingNumShards, + @Nullable IndexMetaData sourceMetaData) { + IndexMetaData.Builder indexMetaDataBuilder = createIndexMetadataBuilder(indexName, sourceMetaData, indexSettings, routingNumShards); + // now, update the mappings with the actual source + Map mappingsMetaData = new HashMap<>(); + DocumentMapper mapper = documentMapperSupplier.get(); + if (mapper != null) { + MappingMetaData mappingMd = new MappingMetaData(mapper); + mappingsMetaData.put(mapper.type(), mappingMd); } - @Override - public void onFailure(String source, Exception e) { - if (e instanceof ResourceAlreadyExistsException) { - logger.trace(() -> new ParameterizedMessage("[{}] failed to create", request.index()), e); - } else { - logger.debug(() -> new ParameterizedMessage("[{}] failed to create", request.index()), e); + for (MappingMetaData mappingMd : mappingsMetaData.values()) { + indexMetaDataBuilder.putMapping(mappingMd); + } + + // apply the aliases in reverse order as the lower index ones have higher order + for (int i = aliases.size() - 1; i >= 0; i--) { + indexMetaDataBuilder.putAlias(aliases.get(i)); + } + + indexMetaDataBuilder.state(IndexMetaData.State.OPEN); + return indexMetaDataBuilder.build(); + } + + /** + * Creates an {@link IndexMetaData.Builder} for the provided index and sets a valid primary term for all the shards if a source + * index meta data is provided (this represents the case where we're shrinking/splitting an index and the primary term for the newly + * created index needs to be gte than the maximum term in the source index). + */ + private static IndexMetaData.Builder createIndexMetadataBuilder(String indexName, @Nullable IndexMetaData sourceMetaData, + Settings indexSettings, int routingNumShards) { + final IndexMetaData.Builder builder = IndexMetaData.builder(indexName); + builder.setRoutingNumShards(routingNumShards); + builder.settings(indexSettings); + + if (sourceMetaData != null) { + /* + * We need to arrange that the primary term on all the shards in the shrunken index is at least as large as + * the maximum primary term on all the shards in the source index. This ensures that we have correct + * document-level semantics regarding sequence numbers in the shrunken index. + */ + final long primaryTerm = + IntStream + .range(0, sourceMetaData.getNumberOfShards()) + .mapToLong(sourceMetaData::primaryTerm) + .max() + .getAsLong(); + for (int shardId = 0; shardId < builder.numberOfShards(); shardId++) { + builder.primaryTerm(shardId, primaryTerm); + } + } + return builder; + } + + private static ClusterBlocks.Builder createClusterBlocksBuilder(ClusterState currentState, String index, Set blocks) { + ClusterBlocks.Builder blocksBuilder = ClusterBlocks.builder().blocks(currentState.blocks()); + if (!blocks.isEmpty()) { + for (ClusterBlock block : blocks) { + blocksBuilder.addIndexBlock(index, block); } - super.onFailure(source, e); } + return blocksBuilder; + } + + private static void updateIndexMappingsAndBuildSortOrder(IndexService indexService, Map mappings, + @Nullable IndexMetaData sourceMetaData) throws IOException { + MapperService mapperService = indexService.mapperService(); + if (!mappings.isEmpty()) { + assert mappings.size() == 1 : mappings; + mapperService.merge(MapperService.SINGLE_MAPPING_NAME, mappings, MergeReason.MAPPING_UPDATE); + } + + if (sourceMetaData == null) { + // now that the mapping is merged we can validate the index sort. + // we cannot validate for index shrinking since the mapping is empty + // at this point. The validation will take place later in the process + // (when all shards are copied in a single place). + indexService.getIndexSortSupplier().get(); + } + } + + private static IndexService validateActiveShardCountAndCreateIndexService(String indexName, ActiveShardCount waitForActiveShards, + Settings indexSettings, int routingNumShards, + IndicesService indicesService) throws IOException { + final IndexMetaData.Builder tmpImdBuilder = IndexMetaData.builder(indexName); + tmpImdBuilder.setRoutingNumShards(routingNumShards); + tmpImdBuilder.settings(indexSettings); + + // Set up everything, now locally create the index to see that things are ok, and apply + IndexMetaData tmpImd = tmpImdBuilder.build(); + if (waitForActiveShards == ActiveShardCount.DEFAULT) { + waitForActiveShards = tmpImd.getWaitForActiveShards(); + } + if (waitForActiveShards.validate(tmpImd.getNumberOfReplicas()) == false) { + throw new IllegalArgumentException("invalid wait_for_active_shards[" + waitForActiveShards + + "]: cannot be greater than number of shard copies [" + + (tmpImd.getNumberOfReplicas() + 1) + "]"); + } + return indicesService.createIndex(tmpImd, Collections.emptyList()); } private void validate(CreateIndexClusterStateUpdateRequest request, ClusterState state) { @@ -596,7 +650,7 @@ private void validate(CreateIndexClusterStateUpdateRequest request, ClusterState } public void validateIndexSettings(String indexName, final Settings settings, final boolean forbidPrivateIndexSettings) - throws IndexCreationException { + throws IndexCreationException { List validationErrors = getIndexSettingsValidationErrors(settings, forbidPrivateIndexSettings); if (validationErrors.isEmpty() == false) { @@ -627,24 +681,44 @@ public static void checkShardLimit(final Settings settings, final ClusterState c } List getIndexSettingsValidationErrors(final Settings settings, final boolean forbidPrivateIndexSettings) { - String customPath = IndexMetaData.INDEX_DATA_PATH_SETTING.get(settings); + List validationErrors = validateIndexCustomPath(settings, env.sharedDataFile()); + if (forbidPrivateIndexSettings) { + validationErrors.addAll(validatePrivateSettingsNotExplicitlySet(settings, indexScopedSettings)); + } + return validationErrors; + } + + private static List validatePrivateSettingsNotExplicitlySet(Settings settings, IndexScopedSettings indexScopedSettings) { List validationErrors = new ArrayList<>(); - if (Strings.isEmpty(customPath) == false && env.sharedDataFile() == null) { - validationErrors.add("path.shared_data must be set in order to use custom data paths"); - } else if (Strings.isEmpty(customPath) == false) { - Path resolvedPath = PathUtils.get(new Path[]{env.sharedDataFile()}, customPath); - if (resolvedPath == null) { - validationErrors.add("custom path [" + customPath + - "] is not a sub-path of path.shared_data [" + env.sharedDataFile() + "]"); + for (final String key : settings.keySet()) { + final Setting setting = indexScopedSettings.get(key); + if (setting == null) { + assert indexScopedSettings.isPrivateSetting(key); + } else if (setting.isPrivateIndex()) { + validationErrors.add("private index setting [" + key + "] can not be set explicitly"); } } - if (forbidPrivateIndexSettings) { - for (final String key : settings.keySet()) { - final Setting setting = indexScopedSettings.get(key); - if (setting == null) { - assert indexScopedSettings.isPrivateSetting(key); - } else if (setting.isPrivateIndex()) { - validationErrors.add("private index setting [" + key + "] can not be set explicitly"); + return validationErrors; + } + + /** + * Validates that the configured index data path (if any) is a sub-path of the configured shared data path (if any) + * + * @param settings the index configured settings + * @param sharedDataPath the configured `path.shared_data` (if any) + * @return a list containing validaton errors or an empty list if there aren't any errors + */ + private static List validateIndexCustomPath(Settings settings, @Nullable Path sharedDataPath) { + String customPath = IndexMetaData.INDEX_DATA_PATH_SETTING.get(settings); + List validationErrors = new ArrayList<>(); + if (!Strings.isEmpty(customPath)) { + if (sharedDataPath == null) { + validationErrors.add("path.shared_data must be set in order to use custom data paths"); + } else { + Path resolvedPath = PathUtils.get(new Path[]{sharedDataPath}, customPath); + if (resolvedPath == null) { + validationErrors.add("custom path [" + customPath + + "] is not a sub-path of path.shared_data [" + sharedDataPath + "]"); } } } @@ -653,11 +727,12 @@ List getIndexSettingsValidationErrors(final Settings settings, final boo /** * Validates the settings and mappings for shrinking an index. + * * @return the list of nodes at least one instance of the source index shards are allocated */ static List validateShrinkIndex(ClusterState state, String sourceIndex, - Set targetIndexMappingsTypes, String targetIndexName, - Settings targetIndexSettings) { + Set targetIndexMappingsTypes, String targetIndexName, + Settings targetIndexSettings) { IndexMetaData sourceMetaData = validateResize(state, sourceIndex, targetIndexMappingsTypes, targetIndexName, targetIndexSettings); assert IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.exists(targetIndexSettings); IndexMetaData.selectShrinkShards(0, sourceMetaData, IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.get(targetIndexSettings)); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexIT.java b/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexIT.java index 7db4bb36dfa30..8592964c766ed 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexIT.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexIT.java @@ -22,6 +22,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.UnavailableShardsException; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; +import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.ActiveShardCount; @@ -36,8 +37,10 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.index.IndexService; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.query.RangeQueryBuilder; +import org.elasticsearch.indices.IndicesService; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.ESIntegTestCase.ClusterScope; import org.elasticsearch.test.ESIntegTestCase.Scope; @@ -49,10 +52,12 @@ import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_WAIT_FOR_ACTIVE_SHARDS; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertBlocked; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertThrows; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.core.IsNull.notNullValue; @@ -287,6 +292,28 @@ public void testRestartIndexCreationAfterFullClusterRestart() throws Exception { ensureGreen("test"); } + public void testFailureToCreateIndexCleansUpIndicesService() { + final int numReplicas = internalCluster().numDataNodes(); + Settings settings = Settings.builder() + .put(IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1) + .put(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), numReplicas) + .build(); + assertAcked(client().admin().indices().prepareCreate("test-idx-1") + .setSettings(settings) + .addAlias(new Alias("alias1").writeIndex(true)) + .get()); + + assertThrows(client().admin().indices().prepareCreate("test-idx-2") + .setSettings(settings) + .addAlias(new Alias("alias1").writeIndex(true)), + IllegalStateException.class); + + IndicesService indicesService = internalCluster().getInstance(IndicesService.class, internalCluster().getMasterName()); + for (IndexService indexService : indicesService) { + assertThat(indexService.index().getName(), not("test-idx-2")); + } + } + /** * This test ensures that index creation adheres to the {@link IndexMetaData#SETTING_WAIT_FOR_ACTIVE_SHARDS}. */ diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/MetaDataIndexTemplateServiceTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/MetaDataIndexTemplateServiceTests.java index a929ee63c576e..5652f20e8b390 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/MetaDataIndexTemplateServiceTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/MetaDataIndexTemplateServiceTests.java @@ -33,6 +33,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.env.Environment; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.indices.InvalidIndexTemplateException; @@ -47,6 +48,7 @@ import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; +import static org.elasticsearch.common.settings.Settings.builder; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; @@ -58,7 +60,7 @@ public void testIndexTemplateInvalidNumberOfShards() { PutRequest request = new PutRequest("test", "test_shards"); request.patterns(Collections.singletonList("test_shards*")); - request.settings(Settings.builder() + request.settings(builder() .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, "0") .put("index.shard.check_on_startup", "blargh").build()); @@ -75,7 +77,7 @@ public void testIndexTemplateInvalidNumberOfShards() { public void testIndexTemplateValidationAccumulatesValidationErrors() { PutRequest request = new PutRequest("test", "putTemplate shards"); request.patterns(Collections.singletonList("_test_shards*")); - request.settings(Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, "0").build()); + request.settings(builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, "0").build()); List throwables = putTemplate(xContentRegistry(), request); assertEquals(throwables.size(), 1); @@ -156,7 +158,7 @@ private static List putTemplate(NamedXContentRegistry xContentRegistr null, null, null, - null, + new Environment(builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()).build(), null), IndexScopedSettings.DEFAULT_SCOPED_SETTINGS, null, xContentRegistry, @@ -189,7 +191,7 @@ private List putTemplateDetail(PutRequest request) throws Exception { indicesService, null, null, - null, + new Environment(builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()).build(), null), null, null, xContentRegistry(), diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexCreationTaskTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexCreationTaskTests.java deleted file mode 100644 index 29ba48139506c..0000000000000 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexCreationTaskTests.java +++ /dev/null @@ -1,488 +0,0 @@ -/* - * 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. - */ -package org.elasticsearch.cluster.metadata; - -import org.apache.logging.log4j.Logger; -import org.apache.lucene.search.Sort; -import org.elasticsearch.Version; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.indices.alias.Alias; -import org.elasticsearch.action.admin.indices.create.CreateIndexClusterStateUpdateRequest; -import org.elasticsearch.action.admin.indices.shrink.ResizeType; -import org.elasticsearch.action.support.ActiveShardCount; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.block.ClusterBlock; -import org.elasticsearch.cluster.block.ClusterBlockLevel; -import org.elasticsearch.cluster.block.ClusterBlocks; -import org.elasticsearch.cluster.node.DiscoveryNodes; -import org.elasticsearch.cluster.routing.IndexRoutingTable; -import org.elasticsearch.cluster.routing.RoutingTable; -import org.elasticsearch.cluster.routing.ShardRoutingState; -import org.elasticsearch.cluster.routing.TestShardRouting; -import org.elasticsearch.cluster.routing.allocation.AllocationService; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.collect.ImmutableOpenMap; -import org.elasticsearch.common.compress.CompressedXContent; -import org.elasticsearch.common.settings.IndexScopedSettings; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.set.Sets; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.index.Index; -import org.elasticsearch.index.IndexService; -import org.elasticsearch.index.mapper.DocumentMapper; -import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.index.mapper.RoutingFieldMapper; -import org.elasticsearch.index.shard.IndexEventListener; -import org.elasticsearch.indices.IndicesService; -import org.elasticsearch.indices.InvalidAliasNameException; -import org.elasticsearch.test.ESTestCase; -import org.hamcrest.Matchers; -import org.mockito.ArgumentCaptor; -import org.mockito.stubbing.Answer; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.Supplier; - -import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS; -import static org.elasticsearch.test.hamcrest.CollectionAssertions.hasAllKeys; -import static org.elasticsearch.test.hamcrest.CollectionAssertions.hasKey; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.startsWith; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.anyMap; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class IndexCreationTaskTests extends ESTestCase { - - private final IndicesService indicesService = mock(IndicesService.class); - private final AliasValidator aliasValidator = new AliasValidator(); - private final NamedXContentRegistry xContentRegistry = mock(NamedXContentRegistry.class); - private final CreateIndexClusterStateUpdateRequest request = mock(CreateIndexClusterStateUpdateRequest.class); - private final Logger logger = mock(Logger.class); - private final AllocationService allocationService = mock(AllocationService.class); - private final MetaDataCreateIndexService.IndexValidator validator = mock(MetaDataCreateIndexService.IndexValidator.class); - private final ActionListener listener = mock(ActionListener.class); - private final ClusterState state = mock(ClusterState.class); - private final Settings.Builder clusterStateSettings = Settings.builder(); - private final MapperService mapper = mock(MapperService.class); - - private final ImmutableOpenMap.Builder tplBuilder = ImmutableOpenMap.builder(); - private final ImmutableOpenMap.Builder customBuilder = ImmutableOpenMap.builder(); - private final ImmutableOpenMap.Builder idxBuilder = ImmutableOpenMap.builder(); - - private final Settings.Builder reqSettings = Settings.builder(); - private final Set reqBlocks = Sets.newHashSet(); - private final MetaData.Builder currentStateMetaDataBuilder = MetaData.builder(); - private final ClusterBlocks currentStateBlocks = mock(ClusterBlocks.class); - private final RoutingTable.Builder routingTableBuilder = RoutingTable.builder(); - private final DocumentMapper docMapper = mock(DocumentMapper.class); - - private ActiveShardCount waitForActiveShardsNum = ActiveShardCount.DEFAULT; - - public void setUp() throws Exception { - super.setUp(); - setupIndicesService(); - setupClusterState(); - when(request.mappings()).thenReturn("{}"); - } - - public void testMatchTemplates() throws Exception { - tplBuilder.put("template_1", createTemplateMetadata("template_1", "te*")); - tplBuilder.put("template_2", createTemplateMetadata("template_2", "tes*")); - tplBuilder.put("template_3", createTemplateMetadata("template_3", "zzz*")); - - final ClusterState result = executeTask(); - - assertThat(result.metaData().index("test").getAliases(), hasAllKeys("alias_from_template_1", "alias_from_template_2")); - assertThat(result.metaData().index("test").getAliases(), not(hasKey("alias_from_template_3"))); - } - - public void testApplyDataFromTemplate() throws Exception { - addMatchingTemplate(builder -> builder - .putAlias(AliasMetaData.builder("alias1")) - .putMapping("_doc", createMapping()) - .settings(Settings.builder().put("key1", "value1")) - ); - - final ClusterState result = executeTask(); - - assertThat(result.metaData().index("test").getAliases(), hasKey("alias1")); - assertThat(result.metaData().index("test").getSettings().get("key1"), equalTo("value1")); - assertThat(getMappingsFromResponse(), Matchers.hasKey("_doc")); - } - - public void testApplyDataFromRequest() throws Exception { - setupRequestAlias(new Alias("alias1")); - setupRequestMapping("type", createMapping()); - reqSettings.put("key1", "value1"); - - final ClusterState result = executeTask(); - - assertThat(result.metaData().index("test").getAliases(), hasKey("alias1")); - assertThat(result.metaData().index("test").getSettings().get("key1"), equalTo("value1")); - assertThat(getMappingsFromResponse(), Matchers.hasKey("_doc")); - } - - public void testInvalidAliasName() throws Exception { - final String[] invalidAliasNames = new String[] { "-alias1", "+alias2", "_alias3", "a#lias", "al:ias", ".", ".." }; - setupRequestAlias(new Alias(randomFrom(invalidAliasNames))); - expectThrows(InvalidAliasNameException.class, this::executeTask); - } - - public void testRequestDataHavePriorityOverTemplateData() throws Exception { - final CompressedXContent tplMapping = createMapping("text"); - final CompressedXContent reqMapping = createMapping("keyword"); - - addMatchingTemplate(builder -> builder - .putAlias(AliasMetaData.builder("alias1").searchRouting("fromTpl").build()) - .putMapping("_doc", tplMapping) - .settings(Settings.builder().put("key1", "tplValue")) - ); - - setupRequestAlias(new Alias("alias1").searchRouting("fromReq")); - setupRequestMapping("type", reqMapping); - reqSettings.put("key1", "reqValue"); - - final ClusterState result = executeTask(); - - assertThat(result.metaData().index("test").getAliases().get("alias1").getSearchRouting(), equalTo("fromReq")); - assertThat(result.metaData().index("test").getSettings().get("key1"), equalTo("reqValue")); - assertThat(getMappingsFromResponse().toString(), equalTo("{_doc={properties={field={type=keyword}}}}")); - } - - public void testDefaultSettings() throws Exception { - final ClusterState result = executeTask(); - - assertThat(result.getMetaData().index("test").getSettings().get(SETTING_NUMBER_OF_SHARDS), equalTo("1")); - } - - public void testSettingsFromClusterState() throws Exception { - clusterStateSettings.put(SETTING_NUMBER_OF_SHARDS, 15); - - final ClusterState result = executeTask(); - - assertThat(result.getMetaData().index("test").getSettings().get(SETTING_NUMBER_OF_SHARDS), equalTo("15")); - } - - public void testTemplateOrder() throws Exception { - addMatchingTemplate(builder -> builder - .order(1) - .settings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 10)) - .putAlias(AliasMetaData.builder("alias1").searchRouting("1").build()) - ); - addMatchingTemplate(builder -> builder - .order(2) - .settings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 11)) - .putAlias(AliasMetaData.builder("alias1").searchRouting("2").build()) - ); - addMatchingTemplate(builder -> builder - .order(3) - .settings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 12)) - .putAlias(AliasMetaData.builder("alias1").searchRouting("3").build()) - ); - final ClusterState result = executeTask(); - - assertThat(result.getMetaData().index("test").getSettings().get(SETTING_NUMBER_OF_SHARDS), equalTo("12")); - assertThat(result.metaData().index("test").getAliases().get("alias1").getSearchRouting(), equalTo("3")); - } - - public void testTemplateOrder2() throws Exception { - addMatchingTemplate(builder -> builder - .order(3) - .settings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 12)) - .putAlias(AliasMetaData.builder("alias1").searchRouting("3").build()) - ); - addMatchingTemplate(builder -> builder - .order(2) - .settings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 11)) - .putAlias(AliasMetaData.builder("alias1").searchRouting("2").build()) - ); - addMatchingTemplate(builder -> builder - .order(1) - .settings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 10)) - .putAlias(AliasMetaData.builder("alias1").searchRouting("1").build()) - ); - final ClusterState result = executeTask(); - - assertThat(result.getMetaData().index("test").getSettings().get(SETTING_NUMBER_OF_SHARDS), equalTo("12")); - assertThat(result.metaData().index("test").getAliases().get("alias1").getSearchRouting(), equalTo("3")); - } - - public void testRequestStateOpen() throws Exception { - executeTask(); - verify(allocationService, times(1)).reroute(anyObject(), anyObject()); - } - - @SuppressWarnings("unchecked") - public void testIndexRemovalOnFailure() throws Exception { - doThrow(new RuntimeException("oops")).when(mapper).merge(anyString(), anyMap(), anyObject()); - - setupRequestMapping("type", createMapping("keyword")); - expectThrows(RuntimeException.class, this::executeTask); - - verify(indicesService, times(1)).removeIndex(anyObject(), anyObject(), anyObject()); - } - - public void testShrinkIndexIgnoresTemplates() throws Exception { - final Index source = new Index("source_idx", "aaa111bbb222"); - - when(request.recoverFrom()).thenReturn(source); - when(request.resizeType()).thenReturn(ResizeType.SHRINK); - currentStateMetaDataBuilder.put(createIndexMetaDataBuilder("source_idx", "aaa111bbb222", 2, 2)); - - routingTableBuilder.add(createIndexRoutingTableWithStartedShards(source)); - - when(currentStateBlocks.indexBlocked(eq(ClusterBlockLevel.WRITE), eq("source_idx"))).thenReturn(true); - reqSettings.put(SETTING_NUMBER_OF_SHARDS, 1); - - addMatchingTemplate(builder -> builder - .putAlias(AliasMetaData.builder("alias1").searchRouting("fromTpl").build()) - .putMapping("mapping1", createMapping()) - .settings(Settings.builder().put("key1", "tplValue")) - ); - - final ClusterState result = executeTask(); - - assertThat(result.metaData().index("test").getAliases(), not(hasKey("alias1"))); - assertThat(result.metaData().index("test").getCustomData(), not(hasKey("custom1"))); - assertThat(result.metaData().index("test").getSettings().keySet(), not(Matchers.contains("key1"))); - } - - public void testValidateWaitForActiveShardsFailure() throws Exception { - waitForActiveShardsNum = ActiveShardCount.from(1000); - - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, this::executeTask); - - assertThat(e.getMessage(), containsString("invalid wait_for_active_shards")); - } - - public void testWriteIndex() throws Exception { - Boolean writeIndex = randomBoolean() ? null : randomBoolean(); - setupRequestAlias(new Alias("alias1").writeIndex(writeIndex)); - setupRequestMapping("type", createMapping()); - reqSettings.put("key1", "value1"); - - final ClusterState result = executeTask(); - assertThat(result.metaData().index("test").getAliases(), hasKey("alias1")); - assertThat(result.metaData().index("test").getAliases().get("alias1").writeIndex(), equalTo(writeIndex)); - } - - public void testWriteIndexValidationException() throws Exception { - IndexMetaData existingWriteIndex = IndexMetaData.builder("test2") - .settings(settings(Version.CURRENT)).putAlias(AliasMetaData.builder("alias1").writeIndex(true).build()) - .numberOfShards(1).numberOfReplicas(0).build(); - idxBuilder.put("test2", existingWriteIndex); - setupRequestMapping("type", createMapping()); - reqSettings.put("key1", "value1"); - setupRequestAlias(new Alias("alias1").writeIndex(true)); - - Exception exception = expectThrows(IllegalStateException.class, () -> executeTask()); - assertThat(exception.getMessage(), startsWith("alias [alias1] has more than one write index [")); - } - - public void testTypedTemplateWithTypelessIndexCreation() throws Exception { - addMatchingTemplate(builder -> builder.putMapping("type", "{\"type\": {}}")); - setupRequestMapping(MapperService.SINGLE_MAPPING_NAME, new CompressedXContent("{\"_doc\":{}}")); - executeTask(); - assertThat(getMappingsFromResponse(), Matchers.hasKey(MapperService.SINGLE_MAPPING_NAME)); - } - - public void testTypedTemplate() throws Exception { - addMatchingTemplate(builder -> builder.putMapping("type", - "{\"type\":{\"properties\":{\"field\":{\"type\":\"keyword\"}}}}")); - executeTask(); - assertThat(getMappingsFromResponse(), Matchers.hasKey("_doc")); - } - - public void testTypelessTemplate() throws Exception { - addMatchingTemplate(builder -> builder.putMapping(MapperService.SINGLE_MAPPING_NAME, "{\"_doc\": {}}")); - executeTask(); - assertThat(getMappingsFromResponse(), Matchers.hasKey(MapperService.SINGLE_MAPPING_NAME)); - } - - private IndexRoutingTable createIndexRoutingTableWithStartedShards(Index index) { - final IndexRoutingTable idxRoutingTable = mock(IndexRoutingTable.class); - - when(idxRoutingTable.getIndex()).thenReturn(index); - when(idxRoutingTable.shardsWithState(eq(ShardRoutingState.STARTED))).thenReturn(Arrays.asList( - TestShardRouting.newShardRouting(index.getName(), 0, "1", randomBoolean(), ShardRoutingState.INITIALIZING).moveToStarted(), - TestShardRouting.newShardRouting(index.getName(), 0, "1", randomBoolean(), ShardRoutingState.INITIALIZING).moveToStarted() - - )); - - return idxRoutingTable; - } - - private IndexMetaData.Builder createIndexMetaDataBuilder(String name, String uuid, int numShards, int numReplicas) { - return IndexMetaData - .builder(name) - .settings(Settings.builder() - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put(IndexMetaData.SETTING_INDEX_UUID, uuid)) - .putMapping(new MappingMetaData(docMapper)) - .numberOfShards(numShards) - .numberOfReplicas(numReplicas); - } - - private interface MetaDataBuilderConfigurator { - void configure(IndexTemplateMetaData.Builder builder) throws IOException; - } - - private void addMatchingTemplate(MetaDataBuilderConfigurator configurator) throws IOException { - final IndexTemplateMetaData.Builder builder = metaDataBuilder("template1", "te*"); - configurator.configure(builder); - - tplBuilder.put("template" + builder.hashCode(), builder.build()); - } - - @SuppressWarnings("unchecked") - private Map> getMappingsFromResponse() throws IOException { - final ArgumentCaptor argument = ArgumentCaptor.forClass(Map.class); - verify(mapper).merge(anyString(), argument.capture(), anyObject()); - return argument.getValue(); - } - - private void setupRequestAlias(Alias alias) { - when(request.aliases()).thenReturn(new HashSet<>(Collections.singletonList(alias))); - } - - private void setupRequestMapping(String mappingKey, CompressedXContent mapping) throws IOException { - when(request.mappings()).thenReturn(mapping.string()); - } - - private CompressedXContent createMapping() throws IOException { - return createMapping("text"); - } - - private CompressedXContent createMapping(String fieldType) throws IOException { - final String mapping = Strings.toString(XContentFactory.jsonBuilder() - .startObject() - .startObject("_doc") - .startObject("properties") - .startObject("field") - .field("type", fieldType) - .endObject() - .endObject() - .endObject() - .endObject()); - - return new CompressedXContent(mapping); - } - - private IndexTemplateMetaData.Builder metaDataBuilder(String name, String pattern) { - return IndexTemplateMetaData - .builder(name) - .patterns(Collections.singletonList(pattern)); - } - - private IndexTemplateMetaData createTemplateMetadata(String name, String pattern) { - return IndexTemplateMetaData - .builder(name) - .patterns(Collections.singletonList(pattern)) - .putAlias(AliasMetaData.builder("alias_from_" + name).build()) - .build(); - } - - @SuppressWarnings("unchecked") - private ClusterState executeTask() throws Exception { - setupState(); - setupRequest(); - final MetaDataCreateIndexService.IndexCreationTask task = new MetaDataCreateIndexService.IndexCreationTask( - logger, allocationService, request, listener, indicesService, aliasValidator, xContentRegistry, clusterStateSettings.build(), - validator, IndexScopedSettings.DEFAULT_SCOPED_SETTINGS) { - - @Override - protected void checkShardLimit(final Settings settings, final ClusterState clusterState) { - // we have to make this a no-op since we are not mocking enough for this method to be able to execute - } - - }; - return task.execute(state); - } - - private void setupState() { - final ImmutableOpenMap.Builder stateCustomsBuilder = ImmutableOpenMap.builder(); - - currentStateMetaDataBuilder - .customs(customBuilder.build()) - .templates(tplBuilder.build()) - .indices(idxBuilder.build()); - - when(state.metaData()).thenReturn(currentStateMetaDataBuilder.build()); - - final ImmutableOpenMap.Builder> blockIdxBuilder = ImmutableOpenMap.builder(); - - when(currentStateBlocks.indices()).thenReturn(blockIdxBuilder.build()); - - when(state.blocks()).thenReturn(currentStateBlocks); - when(state.customs()).thenReturn(stateCustomsBuilder.build()); - when(state.routingTable()).thenReturn(routingTableBuilder.build()); - } - - private void setupRequest() { - when(request.settings()).thenReturn(reqSettings.build()); - when(request.index()).thenReturn("test"); - when(request.waitForActiveShards()).thenReturn(waitForActiveShardsNum); - when(request.blocks()).thenReturn(reqBlocks); - } - - private void setupClusterState() { - final DiscoveryNodes nodes = mock(DiscoveryNodes.class); - when(nodes.getSmallestNonClientNodeVersion()).thenReturn(Version.CURRENT); - - when(state.nodes()).thenReturn(nodes); - } - - @SuppressWarnings("unchecked") - private void setupIndicesService() throws Exception { - final RoutingFieldMapper routingMapper = mock(RoutingFieldMapper.class); - when(routingMapper.required()).thenReturn(false); - - when(docMapper.routingFieldMapper()).thenReturn(routingMapper); - - when(mapper.documentMapper()).thenReturn(docMapper); - - final Index index = new Index("target", "tgt1234"); - final Supplier supplier = mock(Supplier.class); - final IndexService service = mock(IndexService.class); - when(service.index()).thenReturn(index); - when(service.mapperService()).thenReturn(mapper); - when(service.getIndexSortSupplier()).thenReturn(supplier); - when(service.getIndexEventListener()).thenReturn(mock(IndexEventListener.class)); - - when(indicesService.createIndex(anyObject(), anyObject())).thenReturn(service); - when(allocationService.reroute(any(ClusterState.class), anyString())).thenAnswer( - (Answer) invocationOnMock -> (ClusterState) invocationOnMock.getArguments()[0]); - } -} diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexServiceTests.java index 436157ae79b54..e7d35eaeb18ab 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexServiceTests.java @@ -19,8 +19,11 @@ package org.elasticsearch.cluster.metadata; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.ResourceAlreadyExistsException; import org.elasticsearch.Version; +import org.elasticsearch.action.admin.indices.alias.Alias; +import org.elasticsearch.action.admin.indices.create.CreateIndexClusterStateUpdateRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeType; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; @@ -38,40 +41,90 @@ import org.elasticsearch.cluster.shards.ClusterShardLimitIT; import org.elasticsearch.common.Strings; import org.elasticsearch.common.ValidationException; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.indices.InvalidAliasNameException; import org.elasticsearch.indices.InvalidIndexNameException; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.test.gateway.TestGatewayAllocator; +import org.hamcrest.Matchers; +import org.junit.Before; +import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Collections.emptyMap; +import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING; +import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING; +import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_READ_ONLY_BLOCK; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS; +import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_READ_ONLY; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_VERSION_CREATED; +import static org.elasticsearch.cluster.metadata.MetaDataCreateIndexService.aggregateIndexSettings; +import static org.elasticsearch.cluster.metadata.MetaDataCreateIndexService.buildIndexMetaData; +import static org.elasticsearch.cluster.metadata.MetaDataCreateIndexService.clusterStateCreateIndex; +import static org.elasticsearch.cluster.metadata.MetaDataCreateIndexService.getIndexNumberOfRoutingShards; +import static org.elasticsearch.cluster.metadata.MetaDataCreateIndexService.parseMappings; +import static org.elasticsearch.cluster.metadata.MetaDataCreateIndexService.resolveAndValidateAliases; import static org.elasticsearch.cluster.shards.ClusterShardLimitIT.ShardCounts.forDataNodeCount; +import static org.elasticsearch.index.IndexSettings.INDEX_SOFT_DELETES_SETTING; import static org.elasticsearch.indices.IndicesServiceTests.createClusterForShardLimitTest; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.hasValue; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; public class MetaDataCreateIndexServiceTests extends ESTestCase { + private AliasValidator aliasValidator; + private CreateIndexClusterStateUpdateRequest request; + private QueryShardContext queryShardContext; + + @Before + public void setupCreateIndexRequestAndAliasValidator() { + aliasValidator = new AliasValidator(); + request = new CreateIndexClusterStateUpdateRequest("create index", "test", "test"); + Settings indexSettings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1).put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1).build(); + queryShardContext = new QueryShardContext(0, + new IndexSettings(IndexMetaData.builder("test").settings(indexSettings).build(), indexSettings), + BigArrays.NON_RECYCLING_INSTANCE, null, null, null, null, null, xContentRegistry(), writableRegistry(), + null, null, () -> randomNonNegativeLong(), null, null); + } + private ClusterState createClusterState(String name, int numShards, int numReplicas, Settings settings) { int numRoutingShards = settings.getAsInt(IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.getKey(), numShards); MetaData.Builder metaBuilder = MetaData.builder(); @@ -486,4 +539,324 @@ public void testShardLimit() { assertThat(e, hasToString(containsString(expectedMessage))); } + public void testParseMappingsAppliesDataFromTemplateAndRequest() throws Exception { + IndexTemplateMetaData templateMetaData = addMatchingTemplate(templateBuilder -> { + templateBuilder.putAlias(AliasMetaData.builder("alias1")); + templateBuilder.putMapping("_doc", createMapping("mapping_from_template", "text")); + }); + request.mappings(createMapping("mapping_from_request", "text").string()); + + Map parsedMappings = MetaDataCreateIndexService.parseMappings(request.mappings(), List.of(templateMetaData), + NamedXContentRegistry.EMPTY); + + assertThat(parsedMappings, hasKey("_doc")); + Map doc = (Map) parsedMappings.get("_doc"); + assertThat(doc, hasKey("properties")); + Map mappingsProperties = (Map) doc.get("properties"); + assertThat(mappingsProperties, hasKey("mapping_from_request")); + assertThat(mappingsProperties, hasKey("mapping_from_template")); + } + + public void testAggregateSettingsAppliesSettingsFromTemplatesAndRequest() { + IndexTemplateMetaData templateMetaData = addMatchingTemplate(builder -> { + builder.settings(Settings.builder().put("template_setting", "value1")); + }); + ImmutableOpenMap.Builder templatesBuilder = ImmutableOpenMap.builder(); + templatesBuilder.put("template_1", templateMetaData); + MetaData metaData = new MetaData.Builder().templates(templatesBuilder.build()).build(); + ClusterState clusterState = ClusterState.builder(org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING + .getDefault(Settings.EMPTY)) + .metaData(metaData) + .build(); + request.settings(Settings.builder().put("request_setting", "value2").build()); + + Settings aggregatedIndexSettings = aggregateIndexSettings(clusterState, request, List.of(templateMetaData), Map.of(), + null, Settings.EMPTY, IndexScopedSettings.DEFAULT_SCOPED_SETTINGS); + + assertThat(aggregatedIndexSettings.get("template_setting"), equalTo("value1")); + assertThat(aggregatedIndexSettings.get("request_setting"), equalTo("value2")); + } + + public void testInvalidAliasName() { + final String[] invalidAliasNames = new String[] { "-alias1", "+alias2", "_alias3", "a#lias", "al:ias", ".", ".." }; + String aliasName = randomFrom(invalidAliasNames); + request.aliases(Set.of(new Alias(aliasName))); + + expectThrows(InvalidAliasNameException.class, () -> + resolveAndValidateAliases(request.index(), request.aliases(), List.of(), MetaData.builder().build(), + aliasValidator, xContentRegistry(), queryShardContext) + ); + } + + public void testRequestDataHavePriorityOverTemplateData() throws Exception { + CompressedXContent templateMapping = createMapping("test", "text"); + CompressedXContent reqMapping = createMapping("test", "keyword"); + + IndexTemplateMetaData templateMetaData = addMatchingTemplate(builder -> builder + .putAlias(AliasMetaData.builder("alias").searchRouting("fromTemplate").build()) + .putMapping("_doc", templateMapping) + .settings(Settings.builder().put("key1", "templateValue")) + ); + + request.mappings(reqMapping.string()); + request.aliases(Set.of(new Alias("alias").searchRouting("fromRequest"))); + request.settings(Settings.builder().put("key1", "requestValue").build()); + + Map parsedMappings = MetaDataCreateIndexService.parseMappings(request.mappings(), List.of(templateMetaData), + xContentRegistry()); + List resolvedAliases = resolveAndValidateAliases(request.index(), request.aliases(), List.of(templateMetaData), + MetaData.builder().build(), aliasValidator, xContentRegistry(), queryShardContext); + Settings aggregatedIndexSettings = aggregateIndexSettings(ClusterState.EMPTY_STATE, request, List.of(templateMetaData), Map.of(), + null, Settings.EMPTY, IndexScopedSettings.DEFAULT_SCOPED_SETTINGS); + + assertThat(resolvedAliases.get(0).getSearchRouting(), equalTo("fromRequest")); + assertThat(aggregatedIndexSettings.get("key1"), equalTo("requestValue")); + assertThat(parsedMappings, hasKey("_doc")); + Map doc = (Map) parsedMappings.get("_doc"); + assertThat(doc, hasKey("properties")); + Map mappingsProperties = (Map) doc.get("properties"); + assertThat(mappingsProperties, hasKey("test")); + assertThat((Map) mappingsProperties.get("test"), hasValue("keyword")); + } + + public void testDefaultSettings() { + Settings aggregatedIndexSettings = aggregateIndexSettings(ClusterState.EMPTY_STATE, request, List.of(), Map.of(), + null, Settings.EMPTY, IndexScopedSettings.DEFAULT_SCOPED_SETTINGS); + + assertThat(aggregatedIndexSettings.get(SETTING_NUMBER_OF_SHARDS), equalTo("1")); + } + + public void testSettingsFromClusterState() { + Settings aggregatedIndexSettings = aggregateIndexSettings(ClusterState.EMPTY_STATE, request, List.of(), Map.of(), + null, Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 15).build(), IndexScopedSettings.DEFAULT_SCOPED_SETTINGS); + + assertThat(aggregatedIndexSettings.get(SETTING_NUMBER_OF_SHARDS), equalTo("15")); + } + + public void testTemplateOrder() throws Exception { + List templates = new ArrayList<>(3); + templates.add(addMatchingTemplate(builder -> builder + .order(3) + .settings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 12)) + .putAlias(AliasMetaData.builder("alias1").writeIndex(true).searchRouting("3").build()) + )); + templates.add(addMatchingTemplate(builder -> builder + .order(2) + .settings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 11)) + .putAlias(AliasMetaData.builder("alias1").searchRouting("2").build()) + )); + templates.add(addMatchingTemplate(builder -> builder + .order(1) + .settings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 10)) + .putAlias(AliasMetaData.builder("alias1").searchRouting("1").build()) + )); + Settings aggregatedIndexSettings = aggregateIndexSettings(ClusterState.EMPTY_STATE, request, templates, Map.of(), + null, Settings.EMPTY, IndexScopedSettings.DEFAULT_SCOPED_SETTINGS); + List resolvedAliases = resolveAndValidateAliases(request.index(), request.aliases(), templates, + MetaData.builder().build(), aliasValidator, xContentRegistry(), queryShardContext); + assertThat(aggregatedIndexSettings.get(SETTING_NUMBER_OF_SHARDS), equalTo("12")); + AliasMetaData alias = resolvedAliases.get(0); + assertThat(alias.getSearchRouting(), equalTo("3")); + assertThat(alias.writeIndex(), is(true)); + } + + public void testAggregateIndexSettingsIgnoresTemplatesOnCreateFromSourceIndex() throws Exception { + CompressedXContent templateMapping = createMapping("test", "text"); + + IndexTemplateMetaData templateMetaData = addMatchingTemplate(builder -> builder + .putAlias(AliasMetaData.builder("alias").searchRouting("fromTemplate").build()) + .putMapping("_doc", templateMapping) + .settings(Settings.builder().put("templateSetting", "templateValue")) + ); + + request.settings(Settings.builder().put("requestSetting", "requestValue").build()); + request.resizeType(ResizeType.SPLIT); + request.recoverFrom(new Index("sourceIndex", UUID.randomUUID().toString())); + ClusterState clusterState = + createClusterState("sourceIndex", 1, 0, + Settings.builder().put("index.blocks.write", true).build()); + + Settings aggregatedIndexSettings = aggregateIndexSettings(clusterState, request, List.of(templateMetaData), Map.of(), + clusterState.metaData().index("sourceIndex"), Settings.EMPTY, IndexScopedSettings.DEFAULT_SCOPED_SETTINGS); + + assertThat(aggregatedIndexSettings.get("templateSetting"), is(nullValue())); + assertThat(aggregatedIndexSettings.get("requestSetting"), is("requestValue")); + } + + public void testClusterStateCreateIndexThrowsWriteIndexValidationException() throws Exception { + IndexMetaData existingWriteIndex = IndexMetaData.builder("test2") + .settings(settings(Version.CURRENT)).putAlias(AliasMetaData.builder("alias1").writeIndex(true).build()) + .numberOfShards(1).numberOfReplicas(0).build(); + ClusterState currentClusterState = + ClusterState.builder(ClusterState.EMPTY_STATE).metaData(MetaData.builder().put(existingWriteIndex, false).build()).build(); + + IndexMetaData newIndex = IndexMetaData.builder("test") + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(AliasMetaData.builder("alias1").writeIndex(true).build()) + .build(); + + assertThat( + expectThrows(IllegalStateException.class, + () -> clusterStateCreateIndex(currentClusterState, Set.of(), newIndex, (state, reason) -> state)).getMessage(), + startsWith("alias [alias1] has more than one write index [") + ); + } + + public void testClusterStateCreateIndex() { + ClusterState currentClusterState = + ClusterState.builder(ClusterState.EMPTY_STATE).build(); + + IndexMetaData newIndexMetaData = IndexMetaData.builder("test") + .settings(settings(Version.CURRENT).put(SETTING_READ_ONLY, true)) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(AliasMetaData.builder("alias1").writeIndex(true).build()) + .build(); + + // used as a value container, not for the concurrency and visibility guarantees + AtomicBoolean allocationRerouted = new AtomicBoolean(false); + BiFunction rerouteRoutingTable = (clusterState, reason) -> { + allocationRerouted.compareAndSet(false, true); + return clusterState; + }; + + ClusterState updatedClusterState = clusterStateCreateIndex(currentClusterState, Set.of(INDEX_READ_ONLY_BLOCK), newIndexMetaData, + rerouteRoutingTable); + assertThat(updatedClusterState.blocks().getIndexBlockWithId("test", INDEX_READ_ONLY_BLOCK.id()), is(INDEX_READ_ONLY_BLOCK)); + assertThat(updatedClusterState.routingTable().index("test"), is(notNullValue())); + assertThat(allocationRerouted.get(), is(true)); + } + + public void testParseMappingsWithTypedTemplateAndTypelessIndexMapping() throws Exception { + IndexTemplateMetaData templateMetaData = addMatchingTemplate(builder -> { + try { + builder.putMapping("type", "{\"type\": {}}"); + } catch (IOException e) { + ExceptionsHelper.reThrowIfNotNull(e); + } + }); + + Map mappings = parseMappings("{\"_doc\":{}}", List.of(templateMetaData), xContentRegistry()); + assertThat(mappings, Matchers.hasKey(MapperService.SINGLE_MAPPING_NAME)); + } + + public void testParseMappingsWithTypedTemplate() throws Exception { + IndexTemplateMetaData templateMetaData = addMatchingTemplate(builder -> { + try { + builder.putMapping("type", + "{\"type\":{\"properties\":{\"field\":{\"type\":\"keyword\"}}}}"); + } catch (IOException e) { + ExceptionsHelper.reThrowIfNotNull(e); + } + }); + Map mappings = parseMappings("", List.of(templateMetaData), xContentRegistry()); + assertThat(mappings, Matchers.hasKey(MapperService.SINGLE_MAPPING_NAME)); + } + + public void testParseMappingsWithTypelessTemplate() throws Exception { + IndexTemplateMetaData templateMetaData = addMatchingTemplate(builder -> { + try { + builder.putMapping(MapperService.SINGLE_MAPPING_NAME, "{\"_doc\": {}}"); + } catch (IOException e) { + ExceptionsHelper.reThrowIfNotNull(e); + } + }); + Map mappings = parseMappings("", List.of(templateMetaData), xContentRegistry()); + assertThat(mappings, Matchers.hasKey(MapperService.SINGLE_MAPPING_NAME)); + } + + public void testBuildIndexMetadata() { + IndexMetaData sourceIndexMetaData = IndexMetaData.builder("parent") + .settings(Settings.builder() + .put("index.version.created", Version.CURRENT) + .build()) + .numberOfShards(1) + .numberOfReplicas(0) + .primaryTerm(0, 3L) + .build(); + + Settings indexSettings = Settings.builder() + .put("index.version.created", Version.CURRENT) + .put(INDEX_SOFT_DELETES_SETTING.getKey(), false) + .put(SETTING_NUMBER_OF_REPLICAS, 0) + .put(SETTING_NUMBER_OF_SHARDS, 1) + .build(); + List aliases = List.of(AliasMetaData.builder("alias1").build()); + IndexMetaData indexMetaData = buildIndexMetaData("test", aliases, () -> null, indexSettings, 4, sourceIndexMetaData); + + assertThat(indexMetaData.getSettings().getAsBoolean(INDEX_SOFT_DELETES_SETTING.getKey(), true), is(false)); + assertThat(indexMetaData.getAliases().size(), is(1)); + assertThat(indexMetaData.getAliases().keys().iterator().next().value, is("alias1")); + assertThat("The source index primary term must be used", indexMetaData.primaryTerm(0), is(3L)); + } + + public void testGetIndexNumberOfRoutingShardsWithNullSourceIndex() { + Settings indexSettings = Settings.builder() + .put("index.version.created", Version.CURRENT) + .put(INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 3) + .build(); + int targetRoutingNumberOfShards = getIndexNumberOfRoutingShards(indexSettings, null); + assertThat("When the target routing number of shards is not specified the expected value is the configured number of shards " + + "multiplied by 2 at most ten times (ie. 3 * 2^8)", targetRoutingNumberOfShards, is(768)); + } + + public void testGetIndexNumberOfRoutingShardsWhenExplicitlyConfigured() { + Settings indexSettings = Settings.builder() + .put(INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.getKey(), 9) + .put(INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 3) + .build(); + int targetRoutingNumberOfShards = getIndexNumberOfRoutingShards(indexSettings, null); + assertThat(targetRoutingNumberOfShards, is(9)); + } + + public void testGetIndexNumberOfRoutingShardsYieldsSourceNumberOfShards() { + Settings indexSettings = Settings.builder() + .put(INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 3) + .build(); + + IndexMetaData sourceIndexMetaData = IndexMetaData.builder("parent") + .settings(Settings.builder() + .put("index.version.created", Version.CURRENT) + .build()) + .numberOfShards(6) + .numberOfReplicas(0) + .build(); + + int targetRoutingNumberOfShards = getIndexNumberOfRoutingShards(indexSettings, sourceIndexMetaData); + assertThat(targetRoutingNumberOfShards, is(6)); + } + + private IndexTemplateMetaData addMatchingTemplate(Consumer configurator) { + IndexTemplateMetaData.Builder builder = templateMetaDataBuilder("template1", "te*"); + configurator.accept(builder); + return builder.build(); + } + + private IndexTemplateMetaData.Builder templateMetaDataBuilder(String name, String pattern) { + return IndexTemplateMetaData + .builder(name) + .patterns(Collections.singletonList(pattern)); + } + + private CompressedXContent createMapping(String fieldName, String fieldType) { + try { + final String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("_doc") + .startObject("properties") + .startObject(fieldName) + .field("type", fieldType) + .endObject() + .endObject() + .endObject() + .endObject()); + + return new CompressedXContent(mapping); + } catch (IOException e) { + throw ExceptionsHelper.convertToRuntime(e); + } + } + } From 5932d4950d25f0b077c96f235bc07af8a6db1be4 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 18 Dec 2019 08:26:57 -0500 Subject: [PATCH 247/686] Always consume the body in has privileges (#50298) Our REST infrastructure will reject requests that have a body where the body of the request is never consumed. This ensures that we reject requests on endpoints that do not support having a body. This requires cooperation from the REST handlers though, to actually consume the body, otherwise the REST infrastructure will proceed with rejecting the request. This commit addresses an issue in the has privileges API where we would prematurely try to reject a request for not having a username, before consuming the body. Since the body was not consumed, the REST infrastructure would instead reject the request as a bad request. --- .../action/user/RestHasPrivilegesAction.java | 6 ++- .../user/RestHasPrivilegesActionTests.java | 46 ++++++++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java index 975f25a6d2055..3c589e7220aa7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java @@ -69,11 +69,15 @@ public String getName() { @Override public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + /* + * Consume the body immediately. This ensures that if there is a body and we later reject the request (e.g., because security is not + * enabled) that the REST infrastructure will not reject the request for not having consumed the body. + */ + final Tuple content = request.contentOrSourceParam(); final String username = getUsername(request); if (username == null) { return restChannel -> { throw new ElasticsearchSecurityException("there is no authenticated user"); }; } - final Tuple content = request.contentOrSourceParam(); HasPrivilegesRequestBuilder requestBuilder = new HasPrivilegesRequestBuilder(client).source(username, content.v2(), content.v1()); return channel -> requestBuilder.execute(new RestBuilderListener<>(channel) { @Override diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesActionTests.java index 3cf985d9cdf9c..d0173479f37d6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesActionTests.java @@ -6,9 +6,15 @@ package org.elasticsearch.xpack.security.rest.action.user; import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.rest.FakeRestChannel; @@ -23,17 +29,45 @@ public class RestHasPrivilegesActionTests extends ESTestCase { + /* + * Previously we would reject requests that had a body that did not have a username set on the request. This happened because we did not + * consume the body until after checking if there was a username set on the request. If there was not a username set on the request, + * then the body would never be consumed. This means that the REST infrastructure would reject the request as not having a consumed body + * despite the endpoint supporting having a body. Now, we consume the body before checking if there is a username on the request. This + * test ensures that we maintain that behavior. + */ + public void testBodyConsumed() throws Exception { + final XPackLicenseState licenseState = mock(XPackLicenseState.class); + final RestHasPrivilegesAction action = + new RestHasPrivilegesAction(Settings.EMPTY, mock(RestController.class), mock(SecurityContext.class), licenseState); + try (XContentBuilder bodyBuilder = JsonXContent.contentBuilder().startObject().endObject()) { + final RestRequest request = new FakeRestRequest.Builder(xContentRegistry()) + .withPath("/_security/user/_has_privileges/") + .withContent(new BytesArray(bodyBuilder.toString()), XContentType.JSON) + .build(); + final RestChannel channel = new FakeRestChannel(request, true, 1); + action.handleRequest(request, channel, mock(NodeClient.class)); + } + } + public void testBasicLicense() throws Exception { final XPackLicenseState licenseState = mock(XPackLicenseState.class); final RestHasPrivilegesAction action = new RestHasPrivilegesAction(Settings.EMPTY, mock(RestController.class), mock(SecurityContext.class), licenseState); when(licenseState.isSecurityAvailable()).thenReturn(false); - final FakeRestRequest request = new FakeRestRequest(); - final FakeRestChannel channel = new FakeRestChannel(request, true, 1); - action.handleRequest(request, channel, mock(NodeClient.class)); - assertThat(channel.capturedResponse(), notNullValue()); - assertThat(channel.capturedResponse().status(), equalTo(RestStatus.FORBIDDEN)); - assertThat(channel.capturedResponse().content().utf8ToString(), containsString("current license is non-compliant for [security]")); + try (XContentBuilder bodyBuilder = JsonXContent.contentBuilder().startObject().endObject()) { + final RestRequest request = new FakeRestRequest.Builder(xContentRegistry()) + .withPath("/_security/user/_has_privileges/") + .withContent(new BytesArray(bodyBuilder.toString()), XContentType.JSON) + .build(); + final FakeRestChannel channel = new FakeRestChannel(request, true, 1); + action.handleRequest(request, channel, mock(NodeClient.class)); + assertThat(channel.capturedResponse(), notNullValue()); + assertThat(channel.capturedResponse().status(), equalTo(RestStatus.FORBIDDEN)); + assertThat( + channel.capturedResponse().content().utf8ToString(), + containsString("current license is non-compliant for [security]")); + } } } From 36dccaaef0f48e5fb14376e45579c531b85dcd2c Mon Sep 17 00:00:00 2001 From: Kevin Woblick Date: Wed, 18 Dec 2019 15:03:45 +0100 Subject: [PATCH 248/686] [DOCS] Add warning about Docker port exposure (#50169) Docker bypasses the Uncomplicated Firewall (UFW) on Linux by editing the `iptables` config directly, which leads to the exposure of port 9200, even if you blocked it via UFW. This adds a warning along with work-arounds to the docs. Signed-off-by: Kovah --- docs/reference/setup/install/docker.asciidoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/reference/setup/install/docker.asciidoc b/docs/reference/setup/install/docker.asciidoc index add4792bd10af..537dec2904070 100644 --- a/docs/reference/setup/install/docker.asciidoc +++ b/docs/reference/setup/install/docker.asciidoc @@ -87,6 +87,12 @@ endif::[] This sample Docker Compose file brings up a three-node {es} cluster. Node `es01` listens on `localhost:9200` and `es02` and `es03` talk to `es01` over a Docker network. +Please note that this configuration exposes port 9200 on all network interfaces, and given how +Docker manipulates `iptables` on Linux, this means that your {es} cluster is publically accessible, +potentially ignoring any firewall settings. If you don't want to expose port 9200 and instead use +a reverse proxy, replace `9200:9200` with `127.0.0.1:9200:9200` in the docker-compose.yml file. +{es} will then only be accessible from the host machine itself. + The https://docs.docker.com/storage/volumes[Docker named volumes] `data01`, `data02`, and `data03` store the node data directories so the data persists across restarts. If they don't already exist, `docker-compose` creates them when you bring up the cluster. From a456e593c24692b79af38df6c8b734659fa2a14f Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Wed, 18 Dec 2019 17:27:38 +0100 Subject: [PATCH 249/686] Add per-field metadata. (#49419) This PR adds per-field metadata that can be set in the mappings and is later returned by the field capabilities API. This metadata is completely opaque to Elasticsearch but may be used by tools that index data in Elasticsearch to communicate metadata about fields with tools that then search this data. A typical example that has been requested in the past is the ability to attach a unit to a numeric field. In order to not bloat the cluster state, Elasticsearch requires that this metadata be small: - keys can't be longer than 20 chars, - values can only be numbers or strings of no more than 50 chars - no inner arrays or objects, - the metadata can't have more than 5 keys in total. Given that metadata is opaque to Elasticsearch, field capabilities don't try to do anything smart when merging metadata about multiple indices, the union of all field metadatas is returned. Here is how the meta might look like in mappings: ```json { "properties": { "latency": { "type": "long", "meta": { "unit": "ms" } } } } ``` And then in the field capabilities response: ```json { "latency": { "long": { "searchable": true, "aggreggatable": true, "meta": { "unit": [ "ms" ] } } } } ``` When there are no conflicts, values are arrays of size 1, but when there are conflicts, Elasticsearch includes all unique values in this array, without giving ways to know which index has which metadata value: ```json { "latency": { "long": { "searchable": true, "aggreggatable": true, "meta": { "unit": [ "ms", "ns" ] } } } } ``` Closes #33267 --- .../org/elasticsearch/client/SearchIT.java | 6 +- docs/reference/mapping/params.asciidoc | 17 ++-- docs/reference/mapping/params/meta.asciidoc | 31 ++++++ docs/reference/mapping/types/boolean.asciidoc | 3 + docs/reference/mapping/types/date.asciidoc | 4 + docs/reference/mapping/types/keyword.asciidoc | 4 + docs/reference/mapping/types/numeric.asciidoc | 4 + docs/reference/mapping/types/text.asciidoc | 4 + docs/reference/search/field-caps.asciidoc | 6 ++ .../mapper/ScaledFloatFieldMapperTests.java | 31 ++++++ .../test/field_caps/10_basic.yml | 1 - .../rest-api-spec/test/field_caps/20_meta.yml | 65 ++++++++++++ .../test/indices.put_mapping/10_basic.yml | 36 +++++++ .../action/fieldcaps/FieldCapabilities.java | 99 +++++++++++++++++-- .../TransportFieldCapabilitiesAction.java | 4 +- ...TransportFieldCapabilitiesIndexAction.java | 6 +- .../index/mapper/FieldMapper.java | 11 +++ .../index/mapper/MappedFieldType.java | 24 ++++- .../index/mapper/TypeParsers.java | 57 ++++++++++- .../fieldcaps/FieldCapabilitiesTests.java | 75 ++++++++++++-- .../MergedFieldCapabilitiesResponseTests.java | 6 +- .../index/mapper/BooleanFieldMapperTests.java | 28 ++++++ .../index/mapper/DateFieldMapperTests.java | 28 ++++++ .../index/mapper/KeywordFieldMapperTests.java | 27 +++++ .../index/mapper/TextFieldMapperTests.java | 26 +++++ .../index/mapper/TypeParsersTests.java | 92 ++++++++++++++--- .../search/fieldcaps/FieldCapabilitiesIT.java | 16 +-- .../AbstractNumericFieldMapperTestCase.java | 31 ++++++ .../mapper/HistogramFieldMapper.java | 2 + .../mapper/HistogramFieldMapperTests.java | 30 ++++++ .../ExtractedFieldsDetectorTests.java | 2 +- .../analysis/index/IndexResolverTests.java | 14 ++- 32 files changed, 721 insertions(+), 69 deletions(-) create mode 100644 docs/reference/mapping/params/meta.asciidoc create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/20_meta.yml diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java index 9df9623926fc5..1c260d6f91ef7 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java @@ -1229,11 +1229,11 @@ public void testFieldCaps() throws IOException { assertEquals(2, ratingResponse.size()); FieldCapabilities expectedKeywordCapabilities = new FieldCapabilities( - "rating", "keyword", true, true, new String[]{"index2"}, null, null); + "rating", "keyword", true, true, new String[]{"index2"}, null, null, Collections.emptyMap()); assertEquals(expectedKeywordCapabilities, ratingResponse.get("keyword")); FieldCapabilities expectedLongCapabilities = new FieldCapabilities( - "rating", "long", true, true, new String[]{"index1"}, null, null); + "rating", "long", true, true, new String[]{"index1"}, null, null, Collections.emptyMap()); assertEquals(expectedLongCapabilities, ratingResponse.get("long")); // Check the capabilities for the 'field' field. @@ -1242,7 +1242,7 @@ public void testFieldCaps() throws IOException { assertEquals(1, fieldResponse.size()); FieldCapabilities expectedTextCapabilities = new FieldCapabilities( - "field", "text", true, false); + "field", "text", true, false, Collections.emptyMap()); assertEquals(expectedTextCapabilities, fieldResponse.get("text")); } diff --git a/docs/reference/mapping/params.asciidoc b/docs/reference/mapping/params.asciidoc index 51421afb4942e..f4a65aa111df1 100644 --- a/docs/reference/mapping/params.asciidoc +++ b/docs/reference/mapping/params.asciidoc @@ -8,15 +8,15 @@ parameters that are used by <>: The following mapping parameters are common to some or all field datatypes: * <> -* <> * <> * <> * <> * <> * <> +* <> * <> * <> -* <> +* <> * <> * <> * <> @@ -24,7 +24,8 @@ The following mapping parameters are common to some or all field datatypes: * <> * <> * <> -* <> +* <> +* <> * <> * <> * <> @@ -37,8 +38,6 @@ The following mapping parameters are common to some or all field datatypes: include::params/analyzer.asciidoc[] -include::params/normalizer.asciidoc[] - include::params/boost.asciidoc[] include::params/coerce.asciidoc[] @@ -49,10 +48,10 @@ include::params/doc-values.asciidoc[] include::params/dynamic.asciidoc[] -include::params/enabled.asciidoc[] - include::params/eager-global-ordinals.asciidoc[] +include::params/enabled.asciidoc[] + include::params/fielddata.asciidoc[] include::params/format.asciidoc[] @@ -69,8 +68,12 @@ include::params/index-phrases.asciidoc[] include::params/index-prefixes.asciidoc[] +include::params/meta.asciidoc[] + include::params/multi-fields.asciidoc[] +include::params/normalizer.asciidoc[] + include::params/norms.asciidoc[] include::params/null-value.asciidoc[] diff --git a/docs/reference/mapping/params/meta.asciidoc b/docs/reference/mapping/params/meta.asciidoc new file mode 100644 index 0000000000000..52d3ca7ff7cce --- /dev/null +++ b/docs/reference/mapping/params/meta.asciidoc @@ -0,0 +1,31 @@ +[[mapping-field-meta]] +=== `meta` + +Metadata attached to the field. This metadata is opaque to Elasticsearch, it is +only useful for multiple applications that work on the same indices to share +meta information about fields such as units + +[source,console] +------------ +PUT my_index +{ + "mappings": { + "properties": { + "latency": { + "type": "long", + "meta": { + "unit": "ms" + } + } + } + } +} +------------ +// TEST + +NOTE: Field metadata enforces at most 5 entries, that keys have a length that +is less than or equal to 20, and that values are strings whose length is less +than or equal to 50. + +NOTE: Field metadata is updatable by submitting a mapping update. The metadata +of the update will override the metadata of the existing field. diff --git a/docs/reference/mapping/types/boolean.asciidoc b/docs/reference/mapping/types/boolean.asciidoc index 116459d0660e4..ab8011a4c56e4 100644 --- a/docs/reference/mapping/types/boolean.asciidoc +++ b/docs/reference/mapping/types/boolean.asciidoc @@ -120,3 +120,6 @@ The following parameters are accepted by `boolean` fields: the <> field. Accepts `true` or `false` (default). +<>:: + + Metadata about the field. diff --git a/docs/reference/mapping/types/date.asciidoc b/docs/reference/mapping/types/date.asciidoc index 43ede27831b6a..4a9474dcfebf1 100644 --- a/docs/reference/mapping/types/date.asciidoc +++ b/docs/reference/mapping/types/date.asciidoc @@ -137,3 +137,7 @@ The following parameters are accepted by `date` fields: Whether the field value should be stored and retrievable separately from the <> field. Accepts `true` or `false` (default). + +<>:: + + Metadata about the field. diff --git a/docs/reference/mapping/types/keyword.asciidoc b/docs/reference/mapping/types/keyword.asciidoc index 71419b378faae..e0ee14f99b0d0 100644 --- a/docs/reference/mapping/types/keyword.asciidoc +++ b/docs/reference/mapping/types/keyword.asciidoc @@ -115,6 +115,10 @@ The following parameters are accepted by `keyword` fields: when building a query for this field. Accepts `true` or `false` (default). +<>:: + + Metadata about the field. + NOTE: Indexes imported from 2.x do not support `keyword`. Instead they will attempt to downgrade `keyword` into `string`. This allows you to merge modern mappings with legacy mappings. Long lived indexes will have to be recreated diff --git a/docs/reference/mapping/types/numeric.asciidoc b/docs/reference/mapping/types/numeric.asciidoc index 8280945b22777..e99869c7fe275 100644 --- a/docs/reference/mapping/types/numeric.asciidoc +++ b/docs/reference/mapping/types/numeric.asciidoc @@ -149,6 +149,10 @@ The following parameters are accepted by numeric types: the <> field. Accepts `true` or `false` (default). +<>:: + + Metadata about the field. + [[scaled-float-params]] ==== Parameters for `scaled_float` diff --git a/docs/reference/mapping/types/text.asciidoc b/docs/reference/mapping/types/text.asciidoc index 434d91fd4ec6a..f3bbb257fb85a 100644 --- a/docs/reference/mapping/types/text.asciidoc +++ b/docs/reference/mapping/types/text.asciidoc @@ -143,3 +143,7 @@ The following parameters are accepted by `text` fields: Whether term vectors should be stored for an <> field. Defaults to `no`. + +<>:: + + Metadata about the field. diff --git a/docs/reference/search/field-caps.asciidoc b/docs/reference/search/field-caps.asciidoc index f94ac492d7814..c6b945c055bf6 100644 --- a/docs/reference/search/field-caps.asciidoc +++ b/docs/reference/search/field-caps.asciidoc @@ -78,6 +78,12 @@ include::{docdir}/rest-api/common-parms.asciidoc[tag=index-ignore-unavailable] The list of indices where this field is not aggregatable, or null if all indices have the same definition for the field. +`meta`:: + Merged metadata across all indices as a map of string keys to arrays of values. + A value length of 1 indicates that all indices had the same value for this key, + while a length of 2 or more indicates that not all indices had the same value + for this key. + [[search-field-caps-api-example]] ==== {api-examples-title} diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapperTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapperTests.java index 54236c61b76dc..94ae0cee76bb7 100644 --- a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapperTests.java +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapperTests.java @@ -27,6 +27,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.mapper.MapperService.MergeReason; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.InternalSettingsPlugin; @@ -35,6 +36,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import static org.hamcrest.Matchers.containsString; @@ -353,4 +355,33 @@ public void testRejectIndexOptions() throws IOException { MapperParsingException e = expectThrows(MapperParsingException.class, () -> parser.parse("type", new CompressedXContent(mapping))); assertThat(e.getMessage(), containsString("index_options not allowed in field [foo] of type [scaled_float]")); } + + public void testMeta() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "scaled_float") + .field("meta", Collections.singletonMap("foo", "bar")) + .field("scaling_factor", 10.0) + .endObject().endObject().endObject().endObject()); + + DocumentMapper mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE); + assertEquals(mapping, mapper.mappingSource().toString()); + + String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "scaled_float") + .field("scaling_factor", 10.0) + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE); + assertEquals(mapping2, mapper.mappingSource().toString()); + + String mapping3 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "scaled_float") + .field("meta", Collections.singletonMap("baz", "quux")) + .field("scaling_factor", 10.0) + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping3), MergeReason.MAPPING_UPDATE); + assertEquals(mapping3, mapper.mappingSource().toString()); + } } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/10_basic.yml index 137512fe432bd..fcc1ba8104c9d 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/10_basic.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/10_basic.yml @@ -317,4 +317,3 @@ setup: - match: {fields.misc.unmapped.searchable: false} - match: {fields.misc.unmapped.aggregatable: false} - match: {fields.misc.unmapped.indices: ["test2", "test3"]} - diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/20_meta.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/20_meta.yml new file mode 100644 index 0000000000000..c2d565278717a --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/20_meta.yml @@ -0,0 +1,65 @@ +--- +"Merge metadata across multiple indices": + + - skip: + version: " - 7.99.99" + reason: Metadata support was added in 7.6 + + - do: + indices.create: + index: test1 + body: + mappings: + properties: + latency: + type: long + meta: + unit: ms + metric_type: gauge + + - do: + indices.create: + index: test2 + body: + mappings: + properties: + latency: + type: long + meta: + unit: ns + metric_type: gauge + + - do: + indices.create: + index: test3 + + - do: + field_caps: + index: test3 + fields: [latency] + + - is_false: fields.latency.long.meta.unit + + - do: + field_caps: + index: test1 + fields: [latency] + + - match: {fields.latency.long.meta.unit: ["ms"]} + - match: {fields.latency.long.meta.metric_type: ["gauge"]} + + - do: + field_caps: + index: test1,test3 + fields: [latency] + + - match: {fields.latency.long.meta.unit: ["ms"]} + - match: {fields.latency.long.meta.metric_type: ["gauge"]} + + - do: + field_caps: + index: test1,test2,test3 + fields: [latency] + + - match: {fields.latency.long.meta.unit: ["ms", "ns"]} + - match: {fields.latency.long.meta.metric_type: ["gauge"]} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_mapping/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_mapping/10_basic.yml index 4b228ac0ecdb0..3a4044c8d71ad 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_mapping/10_basic.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_mapping/10_basic.yml @@ -108,3 +108,39 @@ - match: { error.type: "illegal_argument_exception" } - match: { error.reason: "Types cannot be provided in put mapping requests, unless the include_type_name parameter is set to true." } + +--- +"Update per-field metadata": + + - skip: + version: " - 7.99.99" + reason: "Per-field meta was introduced in 7.6" + + - do: + indices.create: + index: test_index + body: + mappings: + properties: + foo: + type: keyword + meta: + bar: baz + + - do: + indices.put_mapping: + index: test_index + body: + properties: + foo: + type: keyword + meta: + baz: quux + + - do: + indices.get_mapping: + index: test_index + + - is_false: test_index.mappings.properties.foo.meta.bar + - match: { test_index.mappings.properties.foo.meta.baz: "quux" } + diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java index 20f525716a218..15598a2a88a5c 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java @@ -19,6 +19,7 @@ package org.elasticsearch.action.fieldcaps; +import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -34,20 +35,33 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; /** * Describes the capabilities of a field optionally merged across multiple indices. */ public class FieldCapabilities implements Writeable, ToXContentObject { + private static final ParseField TYPE_FIELD = new ParseField("type"); private static final ParseField SEARCHABLE_FIELD = new ParseField("searchable"); private static final ParseField AGGREGATABLE_FIELD = new ParseField("aggregatable"); private static final ParseField INDICES_FIELD = new ParseField("indices"); private static final ParseField NON_SEARCHABLE_INDICES_FIELD = new ParseField("non_searchable_indices"); private static final ParseField NON_AGGREGATABLE_INDICES_FIELD = new ParseField("non_aggregatable_indices"); + private static final ParseField META_FIELD = new ParseField("meta"); + + private static Map> mapToMapOfSets(Map map) { + final Function, String> entryValueFunction = Map.Entry::getValue; + return map.entrySet().stream().collect( + Collectors.toUnmodifiableMap(Map.Entry::getKey, entryValueFunction.andThen(Set::of))); + } private final String name; private final String type; @@ -58,19 +72,23 @@ public class FieldCapabilities implements Writeable, ToXContentObject { private final String[] nonSearchableIndices; private final String[] nonAggregatableIndices; + private final Map> meta; + /** - * Constructor + * Constructor for a single index. * @param name The name of the field. * @param type The type associated with the field. * @param isSearchable Whether this field is indexed for search. * @param isAggregatable Whether this field can be aggregated on. + * @param meta Metadata about the field. */ - public FieldCapabilities(String name, String type, boolean isSearchable, boolean isAggregatable) { - this(name, type, isSearchable, isAggregatable, null, null, null); + public FieldCapabilities(String name, String type, boolean isSearchable, boolean isAggregatable, + Map meta) { + this(name, type, isSearchable, isAggregatable, null, null, null, mapToMapOfSets(Objects.requireNonNull(meta))); } /** - * Constructor + * Constructor for a set of indices. * @param name The name of the field * @param type The type associated with the field. * @param isSearchable Whether this field is indexed for search. @@ -81,12 +99,14 @@ public FieldCapabilities(String name, String type, boolean isSearchable, boolean * or null if the field is searchable in all indices. * @param nonAggregatableIndices The list of indices where this field is not aggregatable, * or null if the field is aggregatable in all indices. + * @param meta Merged metadata across indices. */ public FieldCapabilities(String name, String type, boolean isSearchable, boolean isAggregatable, String[] indices, String[] nonSearchableIndices, - String[] nonAggregatableIndices) { + String[] nonAggregatableIndices, + Map> meta) { this.name = name; this.type = type; this.isSearchable = isSearchable; @@ -94,6 +114,7 @@ public FieldCapabilities(String name, String type, this.indices = indices; this.nonSearchableIndices = nonSearchableIndices; this.nonAggregatableIndices = nonAggregatableIndices; + this.meta = Objects.requireNonNull(meta); } public FieldCapabilities(StreamInput in) throws IOException { @@ -104,6 +125,11 @@ public FieldCapabilities(StreamInput in) throws IOException { this.indices = in.readOptionalStringArray(); this.nonSearchableIndices = in.readOptionalStringArray(); this.nonAggregatableIndices = in.readOptionalStringArray(); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + meta = in.readMap(StreamInput::readString, i -> i.readSet(StreamInput::readString)); + } else { + meta = Collections.emptyMap(); + } } @Override @@ -115,6 +141,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalStringArray(indices); out.writeOptionalStringArray(nonSearchableIndices); out.writeOptionalStringArray(nonAggregatableIndices); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeMap(meta, StreamOutput::writeString, (o, set) -> o.writeCollection(set, StreamOutput::writeString)); + } } @Override @@ -132,6 +161,17 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (nonAggregatableIndices != null) { builder.field(NON_AGGREGATABLE_INDICES_FIELD.getPreferredName(), nonAggregatableIndices); } + if (meta.isEmpty() == false) { + builder.startObject("meta"); + List>> entries = new ArrayList<>(meta.entrySet()); + entries.sort(Comparator.comparing(Map.Entry::getKey)); // provide predictable order + for (Map.Entry> entry : entries) { + List values = new ArrayList<>(entry.getValue()); + values.sort(String::compareTo); // provide predictable order + builder.field(entry.getKey(), values); + } + builder.endObject(); + } builder.endObject(); return builder; } @@ -150,7 +190,8 @@ public static FieldCapabilities fromXContent(String name, XContentParser parser) (boolean) a[2], a[3] != null ? ((List) a[3]).toArray(new String[0]) : null, a[4] != null ? ((List) a[4]).toArray(new String[0]) : null, - a[5] != null ? ((List) a[5]).toArray(new String[0]) : null)); + a[5] != null ? ((List) a[5]).toArray(new String[0]) : null, + a[6] != null ? ((Map>) a[6]) : Collections.emptyMap())); static { PARSER.declareString(ConstructingObjectParser.constructorArg(), TYPE_FIELD); @@ -159,6 +200,8 @@ public static FieldCapabilities fromXContent(String name, XContentParser parser) PARSER.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), INDICES_FIELD); PARSER.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), NON_SEARCHABLE_INDICES_FIELD); PARSER.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), NON_AGGREGATABLE_INDICES_FIELD); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), + (parser, context) -> parser.map(HashMap::new, p -> Set.copyOf(p.list())), META_FIELD); } /** @@ -213,6 +256,13 @@ public String[] nonAggregatableIndices() { return nonAggregatableIndices; } + /** + * Return merged metadata across indices. + */ + public Map> meta() { + return meta; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -224,12 +274,13 @@ public boolean equals(Object o) { Objects.equals(type, that.type) && Arrays.equals(indices, that.indices) && Arrays.equals(nonSearchableIndices, that.nonSearchableIndices) && - Arrays.equals(nonAggregatableIndices, that.nonAggregatableIndices); + Arrays.equals(nonAggregatableIndices, that.nonAggregatableIndices) && + Objects.equals(meta, that.meta); } @Override public int hashCode() { - int result = Objects.hash(name, type, isSearchable, isAggregatable); + int result = Objects.hash(name, type, isSearchable, isAggregatable, meta); result = 31 * result + Arrays.hashCode(indices); result = 31 * result + Arrays.hashCode(nonSearchableIndices); result = 31 * result + Arrays.hashCode(nonAggregatableIndices); @@ -247,6 +298,7 @@ static class Builder { private boolean isSearchable; private boolean isAggregatable; private List indiceList; + private Map> meta; Builder(String name, String type) { this.name = name; @@ -254,15 +306,38 @@ static class Builder { this.isSearchable = true; this.isAggregatable = true; this.indiceList = new ArrayList<>(); + this.meta = new HashMap<>(); } - void add(String index, boolean search, boolean agg) { + private void add(String index, boolean search, boolean agg) { IndexCaps indexCaps = new IndexCaps(index, search, agg); indiceList.add(indexCaps); this.isSearchable &= search; this.isAggregatable &= agg; } + /** + * Collect capabilities of an index. + */ + void add(String index, boolean search, boolean agg, Map meta) { + add(index, search, agg); + for (Map.Entry entry : meta.entrySet()) { + this.meta.computeIfAbsent(entry.getKey(), key -> new HashSet<>()) + .add(entry.getValue()); + } + } + + /** + * Merge another capabilities instance. + */ + void merge(String index, boolean search, boolean agg, Map> meta) { + add(index, search, agg); + for (Map.Entry> entry : meta.entrySet()) { + this.meta.computeIfAbsent(entry.getKey(), key -> new HashSet<>()) + .addAll(entry.getValue()); + } + } + List getIndices() { return indiceList.stream().map(c -> c.name).collect(Collectors.toList()); } @@ -305,8 +380,12 @@ FieldCapabilities build(boolean withIndices) { } else { nonAggregatableIndices = null; } + final Function>, Set> entryValueFunction = Map.Entry::getValue; + Map> immutableMeta = meta.entrySet().stream() + .collect(Collectors.toUnmodifiableMap( + Map.Entry::getKey, entryValueFunction.andThen(Set::copyOf))); return new FieldCapabilities(name, type, isSearchable, isAggregatable, - indices, nonSearchableIndices, nonAggregatableIndices); + indices, nonSearchableIndices, nonAggregatableIndices, immutableMeta); } } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index 3176d0d31390b..2ddec2c8378c6 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -176,7 +176,7 @@ private void addUnmappedFields(String[] indices, String field, Map> resp Map typeMap = responseMapBuilder.computeIfAbsent(field, f -> new HashMap<>()); FieldCapabilities.Builder builder = typeMap.computeIfAbsent(fieldCap.getType(), key -> new FieldCapabilities.Builder(field, key)); - builder.add(indexName, fieldCap.isSearchable(), fieldCap.isAggregatable()); + builder.merge(indexName, fieldCap.isSearchable(), fieldCap.isAggregatable(), fieldCap.meta()); } } } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java index f391bf82eb944..9b482f60a150c 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java @@ -38,6 +38,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -89,7 +90,8 @@ protected FieldCapabilitiesIndexResponse shardOperation(final FieldCapabilitiesI if (ft != null) { if (indicesService.isMetaDataField(mapperService.getIndexSettings().getIndexVersionCreated(), field) || fieldPredicate.test(ft.name())) { - FieldCapabilities fieldCap = new FieldCapabilities(field, ft.typeName(), ft.isSearchable(), ft.isAggregatable()); + FieldCapabilities fieldCap = new FieldCapabilities(field, ft.typeName(), ft.isSearchable(), ft.isAggregatable(), + ft.meta()); responseMap.put(field, fieldCap); } else { continue; @@ -107,7 +109,7 @@ protected FieldCapabilitiesIndexResponse shardOperation(final FieldCapabilitiesI // no field type, it must be an object field ObjectMapper mapper = mapperService.getObjectMapper(parentField); String type = mapper.nested().isNested() ? "nested" : "object"; - FieldCapabilities fieldCap = new FieldCapabilities(parentField, type, false, false); + FieldCapabilities fieldCap = new FieldCapabilities(parentField, type, false, false, Collections.emptyMap()); responseMap.put(parentField, fieldCap); } dotIndex = parentField.lastIndexOf('.'); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java index 23753b881f20c..57e71ee25ae42 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java @@ -50,6 +50,7 @@ import java.util.Map; import java.util.HashMap; import java.util.Objects; +import java.util.TreeMap; import java.util.stream.StreamSupport; public abstract class FieldMapper extends Mapper implements Cloneable { @@ -223,6 +224,12 @@ protected void setupFieldType(BuilderContext context) { fieldType.setHasDocValues(defaultDocValues); } } + + /** Set metadata on this field. */ + public T meta(Map meta) { + fieldType.setMeta(meta); + return (T) this; + } } protected final Version indexCreatedVersion; @@ -427,6 +434,10 @@ protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, multiFields.toXContent(builder, params); copyTo.toXContent(builder, params); + + if (includeDefaults || fieldType().meta().isEmpty() == false) { + builder.field("meta", new TreeMap<>(fieldType().meta())); // ensure consistent order + } } protected final void doXContentAnalyzers(XContentBuilder builder, boolean includeDefaults) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java b/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java index 82a0239777e30..86dad273e71b0 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java @@ -53,7 +53,9 @@ import java.io.IOException; import java.time.ZoneId; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -72,6 +74,7 @@ public abstract class MappedFieldType extends FieldType { private Object nullValue; private String nullValueAsString; // for sending null value to _all field private boolean eagerGlobalOrdinals; + private Map meta; protected MappedFieldType(MappedFieldType ref) { super(ref); @@ -85,6 +88,7 @@ protected MappedFieldType(MappedFieldType ref) { this.nullValue = ref.nullValue(); this.nullValueAsString = ref.nullValueAsString(); this.eagerGlobalOrdinals = ref.eagerGlobalOrdinals; + this.meta = ref.meta; } public MappedFieldType() { @@ -94,6 +98,7 @@ public MappedFieldType() { setOmitNorms(false); setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS); setBoost(1.0f); + meta = Collections.emptyMap(); } @Override @@ -126,13 +131,14 @@ public boolean equals(Object o) { Objects.equals(eagerGlobalOrdinals, fieldType.eagerGlobalOrdinals) && Objects.equals(nullValue, fieldType.nullValue) && Objects.equals(nullValueAsString, fieldType.nullValueAsString) && - Objects.equals(similarity, fieldType.similarity); + Objects.equals(similarity, fieldType.similarity) && + Objects.equals(meta, fieldType.meta); } @Override public int hashCode() { return Objects.hash(super.hashCode(), name, boost, docValues, indexAnalyzer, searchAnalyzer, searchQuoteAnalyzer, - eagerGlobalOrdinals, similarity == null ? null : similarity.name(), nullValue, nullValueAsString); + eagerGlobalOrdinals, similarity == null ? null : similarity.name(), nullValue, nullValueAsString, meta); } // TODO: we need to override freeze() and add safety checks that all settings are actually set @@ -490,4 +496,18 @@ public static Term extractTerm(Query termQuery) { return ((TermQuery) termQuery).getTerm(); } + /** + * Get the metadata associated with this field. + */ + public Map meta() { + return meta; + } + + /** + * Associate metadata with this field. + */ + public void setMeta(Map meta) { + checkIfFrozen(); + this.meta = Map.copyOf(Objects.requireNonNull(meta)); + } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java b/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java index cabadedcd7f20..4c2fdbdd73725 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java @@ -34,6 +34,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import static org.elasticsearch.common.xcontent.support.XContentMapValues.isArray; import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeFloatValue; @@ -144,7 +146,7 @@ private static void parseAnalyzersAndTermVectors(FieldMapper.Builder builder, St } } - public static void parseNorms(FieldMapper.Builder builder, String fieldName, Object propNode) { + public static void parseNorms(FieldMapper.Builder builder, String fieldName, Object propNode) { builder.omitNorms(XContentMapValues.nodeBooleanValue(propNode, fieldName + ".norms") == false); } @@ -152,8 +154,7 @@ public static void parseNorms(FieldMapper.Builder builder, String fieldName, Obj * Parse text field attributes. In addition to {@link #parseField common attributes} * this will parse analysis and term-vectors related settings. */ - @SuppressWarnings("unchecked") - public static void parseTextField(FieldMapper.Builder builder, String name, Map fieldNode, + public static void parseTextField(FieldMapper.Builder builder, String name, Map fieldNode, Mapper.TypeParser.ParserContext parserContext) { parseField(builder, name, fieldNode, parserContext); parseAnalyzersAndTermVectors(builder, name, fieldNode, parserContext); @@ -168,12 +169,58 @@ public static void parseTextField(FieldMapper.Builder builder, String name, Map< } } + /** + * Parse the {@code meta} key of the mapping. + */ + public static void parseMeta(FieldMapper.Builder builder, String name, Map fieldNode) { + Object metaObject = fieldNode.remove("meta"); + if (metaObject == null) { + // no meta + return; + } + if (metaObject instanceof Map == false) { + throw new MapperParsingException("[meta] must be an object, got " + metaObject.getClass().getSimpleName() + + "[" + metaObject + "] for field [" + name +"]"); + } + @SuppressWarnings("unchecked") + Map meta = (Map) metaObject; + if (meta.size() > 5) { + throw new MapperParsingException("[meta] can't have more than 5 entries, but got " + meta.size() + " on field [" + + name + "]"); + } + for (String key : meta.keySet()) { + if (key.codePointCount(0, key.length()) > 20) { + throw new MapperParsingException("[meta] keys can't be longer than 20 chars, but got [" + key + + "] for field [" + name + "]"); + } + } + for (Object value : meta.values()) { + if (value instanceof String) { + String sValue = (String) value; + if (sValue.codePointCount(0, sValue.length()) > 50) { + throw new MapperParsingException("[meta] values can't be longer than 50 chars, but got [" + value + + "] for field [" + name + "]"); + } + } else if (value == null) { + throw new MapperParsingException("[meta] values can't be null (field [" + name + "])"); + } else { + throw new MapperParsingException("[meta] values can only be strings, but got " + + value.getClass().getSimpleName() + "[" + value + "] for field [" + name + "]"); + } + } + final Function, Object> entryValueFunction = Map.Entry::getValue; + final Function stringCast = String.class::cast; + Map checkedMeta = meta.entrySet().stream() + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, entryValueFunction.andThen(stringCast))); + builder.meta(checkedMeta); + } + /** * Parse common field attributes such as {@code doc_values} or {@code store}. */ - @SuppressWarnings("rawtypes") - public static void parseField(FieldMapper.Builder builder, String name, Map fieldNode, + public static void parseField(FieldMapper.Builder builder, String name, Map fieldNode, Mapper.TypeParser.ParserContext parserContext) { + parseMeta(builder, name, fieldNode); for (Iterator> iterator = fieldNode.entrySet().iterator(); iterator.hasNext();) { Map.Entry entry = iterator.next(); final String propName = entry.getKey(); diff --git a/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java b/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java index deeae3351ec96..6776d66c9df72 100644 --- a/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java +++ b/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java @@ -25,6 +25,9 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Set; import static org.hamcrest.Matchers.equalTo; @@ -48,9 +51,9 @@ protected Writeable.Reader instanceReader() { public void testBuilder() { FieldCapabilities.Builder builder = new FieldCapabilities.Builder("field", "type"); - builder.add("index1", true, false); - builder.add("index2", true, false); - builder.add("index3", true, false); + builder.add("index1", true, false, Collections.emptyMap()); + builder.add("index2", true, false, Collections.emptyMap()); + builder.add("index3", true, false, Collections.emptyMap()); { FieldCapabilities cap1 = builder.build(false); @@ -59,6 +62,7 @@ public void testBuilder() { assertNull(cap1.indices()); assertNull(cap1.nonSearchableIndices()); assertNull(cap1.nonAggregatableIndices()); + assertEquals(Collections.emptyMap(), cap1.meta()); FieldCapabilities cap2 = builder.build(true); assertThat(cap2.isSearchable(), equalTo(true)); @@ -67,12 +71,13 @@ public void testBuilder() { assertThat(cap2.indices(), equalTo(new String[]{"index1", "index2", "index3"})); assertNull(cap2.nonSearchableIndices()); assertNull(cap2.nonAggregatableIndices()); + assertEquals(Collections.emptyMap(), cap2.meta()); } builder = new FieldCapabilities.Builder("field", "type"); - builder.add("index1", false, true); - builder.add("index2", true, false); - builder.add("index3", false, false); + builder.add("index1", false, true, Collections.emptyMap()); + builder.add("index2", true, false, Collections.emptyMap()); + builder.add("index3", false, false, Collections.emptyMap()); { FieldCapabilities cap1 = builder.build(false); assertThat(cap1.isSearchable(), equalTo(false)); @@ -80,6 +85,7 @@ public void testBuilder() { assertNull(cap1.indices()); assertThat(cap1.nonSearchableIndices(), equalTo(new String[]{"index1", "index3"})); assertThat(cap1.nonAggregatableIndices(), equalTo(new String[]{"index2", "index3"})); + assertEquals(Collections.emptyMap(), cap1.meta()); FieldCapabilities cap2 = builder.build(true); assertThat(cap2.isSearchable(), equalTo(false)); @@ -88,6 +94,30 @@ public void testBuilder() { assertThat(cap2.indices(), equalTo(new String[]{"index1", "index2", "index3"})); assertThat(cap2.nonSearchableIndices(), equalTo(new String[]{"index1", "index3"})); assertThat(cap2.nonAggregatableIndices(), equalTo(new String[]{"index2", "index3"})); + assertEquals(Collections.emptyMap(), cap2.meta()); + } + + builder = new FieldCapabilities.Builder("field", "type"); + builder.add("index1", true, true, Collections.emptyMap()); + builder.add("index2", true, true, Map.of("foo", "bar")); + builder.add("index3", true, true, Map.of("foo", "quux")); + { + FieldCapabilities cap1 = builder.build(false); + assertThat(cap1.isSearchable(), equalTo(true)); + assertThat(cap1.isAggregatable(), equalTo(true)); + assertNull(cap1.indices()); + assertNull(cap1.nonSearchableIndices()); + assertNull(cap1.nonAggregatableIndices()); + assertEquals(Map.of("foo", Set.of("bar", "quux")), cap1.meta()); + + FieldCapabilities cap2 = builder.build(true); + assertThat(cap2.isSearchable(), equalTo(true)); + assertThat(cap2.isAggregatable(), equalTo(true)); + assertThat(cap2.indices().length, equalTo(3)); + assertThat(cap2.indices(), equalTo(new String[]{"index1", "index2", "index3"})); + assertNull(cap2.nonSearchableIndices()); + assertNull(cap2.nonAggregatableIndices()); + assertEquals(Map.of("foo", Set.of("bar", "quux")), cap2.meta()); } } @@ -113,9 +143,23 @@ static FieldCapabilities randomFieldCaps(String fieldName) { nonAggregatableIndices[i] = randomAlphaOfLengthBetween(5, 20); } } + + Map> meta; + switch (randomInt(2)) { + case 0: + meta = Collections.emptyMap(); + break; + case 1: + meta = Map.of("foo", Set.of("bar")); + break; + default: + meta = Map.of("foo", Set.of("bar", "baz")); + break; + } + return new FieldCapabilities(fieldName, randomAlphaOfLengthBetween(5, 20), randomBoolean(), randomBoolean(), - indices, nonSearchableIndices, nonAggregatableIndices); + indices, nonSearchableIndices, nonAggregatableIndices, meta); } @Override @@ -127,7 +171,8 @@ protected FieldCapabilities mutateInstance(FieldCapabilities instance) { String[] indices = instance.indices(); String[] nonSearchableIndices = instance.nonSearchableIndices(); String[] nonAggregatableIndices = instance.nonAggregatableIndices(); - switch (between(0, 6)) { + Map> meta = instance.meta(); + switch (between(0, 7)) { case 0: name += randomAlphaOfLengthBetween(1, 10); break; @@ -169,7 +214,6 @@ protected FieldCapabilities mutateInstance(FieldCapabilities instance) { nonSearchableIndices = newNonSearchableIndices; break; case 6: - default: String[] newNonAggregatableIndices; int startNonAggregatablePos = 0; if (nonAggregatableIndices == null) { @@ -183,7 +227,18 @@ protected FieldCapabilities mutateInstance(FieldCapabilities instance) { } nonAggregatableIndices = newNonAggregatableIndices; break; + case 7: + Map> newMeta; + if (meta.isEmpty()) { + newMeta = Map.of("foo", Set.of("bar")); + } else { + newMeta = Collections.emptyMap(); + } + meta = newMeta; + break; + default: + throw new AssertionError(); } - return new FieldCapabilities(name, type, isSearchable, isAggregatable, indices, nonSearchableIndices, nonAggregatableIndices); + return new FieldCapabilities(name, type, isSearchable, isAggregatable, indices, nonSearchableIndices, nonAggregatableIndices, meta); } } diff --git a/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java b/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java index 656dd5458e809..13a634177650c 100644 --- a/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java @@ -152,19 +152,19 @@ public void testEmptyResponse() throws IOException { private static FieldCapabilitiesResponse createSimpleResponse() { Map titleCapabilities = new HashMap<>(); - titleCapabilities.put("text", new FieldCapabilities("title", "text", true, false)); + titleCapabilities.put("text", new FieldCapabilities("title", "text", true, false, Collections.emptyMap())); Map ratingCapabilities = new HashMap<>(); ratingCapabilities.put("long", new FieldCapabilities("rating", "long", true, false, new String[]{"index1", "index2"}, null, - new String[]{"index1"})); + new String[]{"index1"}, Collections.emptyMap())); ratingCapabilities.put("keyword", new FieldCapabilities("rating", "keyword", false, true, new String[]{"index3", "index4"}, new String[]{"index4"}, - null)); + null, Collections.emptyMap())); Map> responses = new HashMap<>(); responses.put("title", titleCapabilities); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldMapperTests.java index a6e6d5c79d1e8..1fd3f51ece916 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldMapperTests.java @@ -38,6 +38,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.mapper.MapperService.MergeReason; import org.elasticsearch.index.mapper.ParseContext.Document; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; @@ -46,6 +47,7 @@ import java.io.IOException; import java.util.Collection; +import java.util.Collections; import static org.hamcrest.Matchers.containsString; @@ -251,4 +253,30 @@ public void testEmptyName() throws IOException { ); assertThat(e.getMessage(), containsString("name cannot be empty string")); } + + public void testMeta() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "boolean") + .field("meta", Collections.singletonMap("foo", "bar")) + .endObject().endObject().endObject().endObject()); + + DocumentMapper mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE); + assertEquals(mapping, mapper.mappingSource().toString()); + + String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "boolean") + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE); + assertEquals(mapping2, mapper.mappingSource().toString()); + + String mapping3 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "boolean") + .field("meta", Collections.singletonMap("baz", "quux")) + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping3), MergeReason.MAPPING_UPDATE); + assertEquals(mapping3, mapper.mappingSource().toString()); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java index daa70c865133f..477cf7de8285c 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java @@ -29,6 +29,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.mapper.MapperService.MergeReason; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.InternalSettingsPlugin; @@ -39,6 +40,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Collection; +import java.util.Collections; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.notNullValue; @@ -415,4 +417,30 @@ public void testIllegalFormatField() throws Exception { () -> parser.parse("type", new CompressedXContent(mapping))); assertEquals("Invalid format: [[test_format]]: Unknown pattern letter: t", e.getMessage()); } + + public void testMeta() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "date") + .field("meta", Collections.singletonMap("foo", "bar")) + .endObject().endObject().endObject().endObject()); + + DocumentMapper mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE); + assertEquals(mapping, mapper.mappingSource().toString()); + + String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "date") + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE); + assertEquals(mapping2, mapper.mappingSource().toString()); + + String mapping3 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "date") + .field("meta", Collections.singletonMap("baz", "quux")) + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping3), MergeReason.MAPPING_UPDATE); + assertEquals(mapping3, mapper.mappingSource().toString()); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java index dad4b72cdf380..fd821f1170ce8 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java @@ -46,6 +46,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -547,4 +548,30 @@ public void testSplitQueriesOnWhitespace() throws IOException { assertThat(ft.searchAnalyzer().name(), equalTo("my_lowercase")); assertTokenStreamContents(ft.searchAnalyzer().analyzer().tokenStream("", "Hello World"), new String[] {"hello world"}); } + + public void testMeta() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "keyword") + .field("meta", Collections.singletonMap("foo", "bar")) + .endObject().endObject().endObject().endObject()); + + DocumentMapper mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE); + assertEquals(mapping, mapper.mappingSource().toString()); + + String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "keyword") + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE); + assertEquals(mapping2, mapper.mappingSource().toString()); + + String mapping3 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "keyword") + .field("meta", Collections.singletonMap("baz", "quux")) + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping3), MergeReason.MAPPING_UPDATE); + assertEquals(mapping3, mapper.mappingSource().toString()); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java index 5d8d45687b1be..539d8bbdeaf59 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java @@ -1247,4 +1247,30 @@ public void testSimpleMerge() throws IOException { assertThat(mapper.mappers().getMapper("b_field"), instanceOf(KeywordFieldMapper.class)); } } + + public void testMeta() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "text") + .field("meta", Collections.singletonMap("foo", "bar")) + .endObject().endObject().endObject().endObject()); + + DocumentMapper mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE); + assertEquals(mapping, mapper.mappingSource().toString()); + + String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "text") + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE); + assertEquals(mapping2, mapper.mappingSource().toString()); + + String mapping3 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "text") + .field("meta", Collections.singletonMap("baz", "quux")) + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping3), MergeReason.MAPPING_UPDATE); + assertEquals(mapping3, mapper.mappingSource().toString()); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/TypeParsersTests.java b/server/src/test/java/org/elasticsearch/index/mapper/TypeParsersTests.java index 8fb30cf8d1c38..1a03cc7ca91b4 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/TypeParsersTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/TypeParsersTests.java @@ -19,6 +19,20 @@ package org.elasticsearch.index.mapper; +import static org.elasticsearch.index.analysis.AnalysisRegistry.DEFAULT_ANALYZER_NAME; +import static org.elasticsearch.index.analysis.AnalysisRegistry.DEFAULT_SEARCH_ANALYZER_NAME; +import static org.elasticsearch.index.analysis.AnalysisRegistry.DEFAULT_SEARCH_QUOTED_ANALYZER_NAME; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.standard.StandardAnalyzer; @@ -40,18 +54,7 @@ import org.elasticsearch.index.analysis.TokenFilterFactory; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import static org.elasticsearch.index.analysis.AnalysisRegistry.DEFAULT_ANALYZER_NAME; -import static org.elasticsearch.index.analysis.AnalysisRegistry.DEFAULT_SEARCH_ANALYZER_NAME; -import static org.elasticsearch.index.analysis.AnalysisRegistry.DEFAULT_SEARCH_QUOTED_ANALYZER_NAME; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import org.hamcrest.Matchers; public class TypeParsersTests extends ESTestCase { @@ -227,4 +230,69 @@ public TokenStream create(TokenStream tokenStream) { return new CustomAnalyzer(null, new CharFilterFactory[0], new TokenFilterFactory[] { tokenFilter }); } + + public void testParseMeta() { + FieldMapper.Builder builder = new KeywordFieldMapper.Builder("foo"); + Mapper.TypeParser.ParserContext parserContext = new Mapper.TypeParser.ParserContext(null, null, null, null, null); + + { + Map mapping = new HashMap<>(Map.of("meta", 3)); + MapperParsingException e = expectThrows(MapperParsingException.class, + () -> TypeParsers.parseField(builder, builder.name, mapping, parserContext)); + assertEquals("[meta] must be an object, got Integer[3] for field [foo]", e.getMessage()); + } + + { + Map mapping = new HashMap<>(Map.of("meta", Map.of("veryloooooooooooongkey", 3L))); + MapperParsingException e = expectThrows(MapperParsingException.class, + () -> TypeParsers.parseField(builder, builder.name, mapping, parserContext)); + assertEquals("[meta] keys can't be longer than 20 chars, but got [veryloooooooooooongkey] for field [foo]", + e.getMessage()); + } + + { + Map mapping = new HashMap<>(Map.of("meta", Map.of( + "foo1", 3L, "foo2", 4L, "foo3", 5L, "foo4", 6L, "foo5", 7L, "foo6", 8L))); + MapperParsingException e = expectThrows(MapperParsingException.class, + () -> TypeParsers.parseField(builder, builder.name, mapping, parserContext)); + assertEquals("[meta] can't have more than 5 entries, but got 6 on field [foo]", + e.getMessage()); + } + + { + Map mapping = new HashMap<>(Map.of("meta", Map.of("foo", Map.of("bar", "baz")))); + MapperParsingException e = expectThrows(MapperParsingException.class, + () -> TypeParsers.parseField(builder, builder.name, mapping, parserContext)); + assertEquals("[meta] values can only be strings, but got Map1[{bar=baz}] for field [foo]", + e.getMessage()); + } + + { + Map mapping = new HashMap<>(Map.of("meta", Map.of("bar", "baz", "foo", 3))); + MapperParsingException e = expectThrows(MapperParsingException.class, + () -> TypeParsers.parseField(builder, builder.name, mapping, parserContext)); + assertEquals("[meta] values can only be strings, but got Integer[3] for field [foo]", + e.getMessage()); + } + + { + Map meta = new HashMap<>(); + meta.put("foo", null); + Map mapping = new HashMap<>(Map.of("meta", meta)); + MapperParsingException e = expectThrows(MapperParsingException.class, + () -> TypeParsers.parseField(builder, builder.name, mapping, parserContext)); + assertEquals("[meta] values can't be null (field [foo])", + e.getMessage()); + } + + { + String longString = IntStream.range(0, 51) + .mapToObj(Integer::toString) + .collect(Collectors.joining()); + Map mapping = new HashMap<>(Map.of("meta", Map.of("foo", longString))); + MapperParsingException e = expectThrows(MapperParsingException.class, + () -> TypeParsers.parseField(builder, builder.name, mapping, parserContext)); + assertThat(e.getMessage(), Matchers.startsWith("[meta] values can't be longer than 50 chars")); + } + } } diff --git a/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java b/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java index 7adc447a20736..254447abcf8eb 100644 --- a/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java +++ b/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java @@ -119,12 +119,14 @@ public void testFieldAlias() { assertTrue(distance.containsKey("double")); assertEquals( - new FieldCapabilities("distance", "double", true, true, new String[] {"old_index"}, null, null), + new FieldCapabilities("distance", "double", true, true, new String[] {"old_index"}, null, null, + Collections.emptyMap()), distance.get("double")); assertTrue(distance.containsKey("text")); assertEquals( - new FieldCapabilities("distance", "text", true, false, new String[] {"new_index"}, null, null), + new FieldCapabilities("distance", "text", true, false, new String[] {"new_index"}, null, null, + Collections.emptyMap()), distance.get("text")); // Check the capabilities for the 'route_length_miles' alias. @@ -133,7 +135,7 @@ public void testFieldAlias() { assertTrue(routeLength.containsKey("double")); assertEquals( - new FieldCapabilities("route_length_miles", "double", true, true), + new FieldCapabilities("route_length_miles", "double", true, true, Collections.emptyMap()), routeLength.get("double")); } @@ -174,12 +176,14 @@ public void testWithUnmapped() { assertTrue(oldField.containsKey("long")); assertEquals( - new FieldCapabilities("old_field", "long", true, true, new String[] {"old_index"}, null, null), + new FieldCapabilities("old_field", "long", true, true, new String[] {"old_index"}, null, null, + Collections.emptyMap()), oldField.get("long")); assertTrue(oldField.containsKey("unmapped")); assertEquals( - new FieldCapabilities("old_field", "unmapped", false, false, new String[] {"new_index"}, null, null), + new FieldCapabilities("old_field", "unmapped", false, false, new String[] {"new_index"}, null, null, + Collections.emptyMap()), oldField.get("unmapped")); Map newField = response.getField("new_field"); @@ -187,7 +191,7 @@ public void testWithUnmapped() { assertTrue(newField.containsKey("long")); assertEquals( - new FieldCapabilities("new_field", "long", true, true), + new FieldCapabilities("new_field", "long", true, true, Collections.emptyMap()), newField.get("long")); } diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/AbstractNumericFieldMapperTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/AbstractNumericFieldMapperTestCase.java index b732c6b5b42bf..18e3a64648759 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/AbstractNumericFieldMapperTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/AbstractNumericFieldMapperTestCase.java @@ -22,6 +22,7 @@ import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.mapper.MapperService.MergeReason; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.InternalSettingsPlugin; @@ -29,6 +30,7 @@ import java.io.IOException; import java.util.Collection; +import java.util.Collections; import java.util.Set; import static org.hamcrest.Matchers.containsString; @@ -124,4 +126,33 @@ public void testEmptyName() throws IOException { } } + public void testMeta() throws Exception { + for (String type : TYPES) { + IndexService indexService = createIndex("test-" + type); + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", type) + .field("meta", Collections.singletonMap("foo", "bar")) + .endObject().endObject().endObject().endObject()); + + DocumentMapper mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE); + assertEquals(mapping, mapper.mappingSource().toString()); + + String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", type) + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE); + assertEquals(mapping2, mapper.mappingSource().toString()); + + String mapping3 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", type) + .field("meta", Collections.singletonMap("baz", "quux")) + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping3), MergeReason.MAPPING_UPDATE); + assertEquals(mapping3, mapper.mappingSource().toString()); + } + } + } diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java index b22f6eb0573df..0e5f9a9225178 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java @@ -43,6 +43,7 @@ import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.ParseContext; +import org.elasticsearch.index.mapper.TypeParsers; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.index.query.QueryShardException; import org.elasticsearch.indices.breaker.CircuitBreakerService; @@ -124,6 +125,7 @@ public Mapper.Builder parse(String name, Map node, ParserContext parserContext) throws MapperParsingException { Builder builder = new HistogramFieldMapper.Builder(name); + TypeParsers.parseMeta(builder, name, node); for (Iterator> iterator = node.entrySet().iterator(); iterator.hasNext();) { Map.Entry entry = iterator.next(); String propName = entry.getKey(); diff --git a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapperTests.java b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapperTests.java index 055b01186cd61..76aa163d7e98e 100644 --- a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapperTests.java +++ b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapperTests.java @@ -11,10 +11,12 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.IndexService; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.SourceToParse; +import org.elasticsearch.index.mapper.MapperService.MergeReason; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.xpack.analytics.AnalyticsPlugin; @@ -22,6 +24,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import static org.hamcrest.Matchers.containsString; @@ -498,6 +501,33 @@ public void testNegativeCount() throws Exception { assertThat(e.getCause().getMessage(), containsString("[counts] elements must be >= 0 but got -3")); } + public void testMeta() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "histogram") + .field("meta", Collections.singletonMap("foo", "bar")) + .endObject().endObject().endObject().endObject()); + + IndexService indexService = createIndex("test"); + DocumentMapper mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE); + assertEquals(mapping, mapper.mappingSource().toString()); + + String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "histogram") + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE); + assertEquals(mapping2, mapper.mappingSource().toString()); + + String mapping3 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("field").field("type", "histogram") + .field("meta", Collections.singletonMap("baz", "quux")) + .endObject().endObject().endObject().endObject()); + mapper = indexService.mapperService().merge("_doc", + new CompressedXContent(mapping3), MergeReason.MAPPING_UPDATE); + assertEquals(mapping3, mapper.mappingSource().toString()); + } + @Override protected Collection> getPlugins() { List> plugins = new ArrayList<>(super.getPlugins()); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java index 6b882d03f2919..6a273a72b254f 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java @@ -953,7 +953,7 @@ private MockFieldCapsResponseBuilder addNonAggregatableField(String field, Strin private MockFieldCapsResponseBuilder addField(String field, boolean isAggregatable, String... types) { Map caps = new HashMap<>(); for (String type : types) { - caps.put(type, new FieldCapabilities(field, type, true, isAggregatable)); + caps.put(type, new FieldCapabilities(field, type, true, isAggregatable, Collections.emptyMap())); } fieldCaps.put(field, caps); return this; diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolverTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolverTests.java index d57c090817d10..045f2b8928f33 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolverTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolverTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.sql.type.TypesTests; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -143,8 +144,10 @@ public void testMergeIncompatibleCapabilitiesOfObjectFields() throws Exception { addFieldCaps(fieldCaps, fieldName + ".keyword", "keyword", true, true); Map multi = new HashMap<>(); - multi.put("long", new FieldCapabilities(fieldName, "long", true, true, new String[] { "one-index" }, null, null)); - multi.put("text", new FieldCapabilities(fieldName, "text", true, false, new String[] { "another-index" }, null, null)); + multi.put("long", new FieldCapabilities(fieldName, "long", true, true, new String[] { "one-index" }, null, null, + Collections.emptyMap())); + multi.put("text", new FieldCapabilities(fieldName, "text", true, false, new String[] { "another-index" }, null, null, + Collections.emptyMap())); fieldCaps.put(fieldName, multi); @@ -214,7 +217,8 @@ public void testMultipleCompatibleIndicesWithDifferentFields() { public void testIndexWithNoMapping() { Map> versionFC = singletonMap("_version", - singletonMap("_index", new FieldCapabilities("_version", "_version", false, false))); + singletonMap("_index", new FieldCapabilities("_version", "_version", false, false, + Collections.emptyMap()))); assertTrue(IndexResolver.mergedMappings("*", new String[] { "empty" }, versionFC).isValid()); } @@ -289,7 +293,7 @@ private static class UpdateableFieldCapabilities extends FieldCapabilities { List nonAggregatableIndices = new ArrayList<>(); UpdateableFieldCapabilities(String name, String type, boolean isSearchable, boolean isAggregatable) { - super(name, type, isSearchable, isAggregatable); + super(name, type, isSearchable, isAggregatable, Collections.emptyMap()); } @Override @@ -323,7 +327,7 @@ private static void assertEqualsMaps(Map left, Map right) { private void addFieldCaps(Map> fieldCaps, String name, String type, boolean isSearchable, boolean isAggregatable) { Map cap = new HashMap<>(); - cap.put(name, new FieldCapabilities(name, type, isSearchable, isAggregatable)); + cap.put(name, new FieldCapabilities(name, type, isSearchable, isAggregatable, Collections.emptyMap())); fieldCaps.put(name, cap); } } From 0a4416b9ddbccd8acf43f4af82f37aae0109b9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Wed, 18 Dec 2019 17:35:35 +0100 Subject: [PATCH 250/686] [Docs] Remove `intervals` filter rule from allowed top-level rules (#50320) The `filter` rule is not allowed on the top-level of the query, so removing it from the list of allowed rules. Where it can be nested inside other rules, those rules already mention it. --- docs/reference/query-dsl/intervals-query.asciidoc | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/reference/query-dsl/intervals-query.asciidoc b/docs/reference/query-dsl/intervals-query.asciidoc index 150b4b9616c16..9f9280c80a484 100644 --- a/docs/reference/query-dsl/intervals-query.asciidoc +++ b/docs/reference/query-dsl/intervals-query.asciidoc @@ -75,7 +75,6 @@ Valid rules include: * <> * <> * <> -* <> -- [[intervals-match]] From 672c16f17cd7fd65edd44e259c14eb1f77fc601b Mon Sep 17 00:00:00 2001 From: Nikita Glashenko Date: Wed, 18 Dec 2019 20:40:45 +0400 Subject: [PATCH 251/686] Add tests for IntervalsSourceProvider.Wildcard and Prefix (#50306) This PR adds unit tests for wire and xContent serialization of `IntervalsSourceProvider.Wildcard` and `IntervalsSourceProvider.Prefix`. Relates #50150 --- .../index/query/IntervalsSourceProvider.java | 34 +++++++-- .../PrefixIntervalsSourceProviderTests.java | 76 +++++++++++++++++++ .../WildcardIntervalsSourceProviderTests.java | 76 +++++++++++++++++++ 3 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/index/query/PrefixIntervalsSourceProviderTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/query/WildcardIntervalsSourceProviderTests.java diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java index 167266e0892fd..4918d7c7c7f3f 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java @@ -87,7 +87,7 @@ public static IntervalsSourceProvider fromXContent(XContentParser parser) throws return Wildcard.fromXContent(parser); } throw new ParsingException(parser.getTokenLocation(), - "Unknown interval type [" + parser.currentName() + "], expecting one of [match, any_of, all_of, prefix]"); + "Unknown interval type [" + parser.currentName() + "], expecting one of [match, any_of, all_of, prefix, wildcard]"); } private static IntervalsSourceProvider parseInnerIntervals(XContentParser parser) throws IOException { @@ -548,6 +548,18 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public static Prefix fromXContent(XContentParser parser) throws IOException { return PARSER.parse(parser, null); } + + String getPrefix() { + return prefix; + } + + String getAnalyzer() { + return analyzer; + } + + String getUseField() { + return useField; + } } public static class Wildcard extends IntervalsSourceProvider { @@ -613,10 +625,10 @@ public void extractFields(Set fields) { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Prefix prefix = (Prefix) o; - return Objects.equals(pattern, prefix.prefix) && - Objects.equals(analyzer, prefix.analyzer) && - Objects.equals(useField, prefix.useField); + Wildcard wildcard = (Wildcard) o; + return Objects.equals(pattern, wildcard.pattern) && + Objects.equals(analyzer, wildcard.analyzer) && + Objects.equals(useField, wildcard.useField); } @Override @@ -665,6 +677,18 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public static Wildcard fromXContent(XContentParser parser) throws IOException { return PARSER.parse(parser, null); } + + String getPattern() { + return pattern; + } + + String getAnalyzer() { + return analyzer; + } + + String getUseField() { + return useField; + } } static class ScriptFilterSource extends FilteredIntervalsSource { diff --git a/server/src/test/java/org/elasticsearch/index/query/PrefixIntervalsSourceProviderTests.java b/server/src/test/java/org/elasticsearch/index/query/PrefixIntervalsSourceProviderTests.java new file mode 100644 index 0000000000000..8f2d950fbff1b --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/PrefixIntervalsSourceProviderTests.java @@ -0,0 +1,76 @@ +/* + * 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. + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; + +import static org.elasticsearch.index.query.IntervalsSourceProvider.Prefix; + +public class PrefixIntervalsSourceProviderTests extends AbstractSerializingTestCase { + + @Override + protected Prefix createTestInstance() { + return new Prefix( + randomAlphaOfLength(10), + randomBoolean() ? randomAlphaOfLength(10) : null, + randomBoolean() ? randomAlphaOfLength(10) : null + ); + } + + @Override + protected Prefix mutateInstance(Prefix instance) throws IOException { + String prefix = instance.getPrefix(); + String analyzer = instance.getAnalyzer(); + String useField = instance.getUseField(); + switch (between(0, 2)) { + case 0: + prefix += "a"; + break; + case 1: + analyzer = randomAlphaOfLength(5); + break; + case 2: + useField = useField == null ? randomAlphaOfLength(5) : null; + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + return new Prefix(prefix, analyzer, useField); + } + + @Override + protected Writeable.Reader instanceReader() { + return Prefix::new; + } + + @Override + protected Prefix doParseInstance(XContentParser parser) throws IOException { + if (parser.nextToken() == XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + Prefix prefix = (Prefix) IntervalsSourceProvider.fromXContent(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + return prefix; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/query/WildcardIntervalsSourceProviderTests.java b/server/src/test/java/org/elasticsearch/index/query/WildcardIntervalsSourceProviderTests.java new file mode 100644 index 0000000000000..7bcf3defeea9c --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/WildcardIntervalsSourceProviderTests.java @@ -0,0 +1,76 @@ +/* + * 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. + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; + +import static org.elasticsearch.index.query.IntervalsSourceProvider.Wildcard; + +public class WildcardIntervalsSourceProviderTests extends AbstractSerializingTestCase { + + @Override + protected Wildcard createTestInstance() { + return new Wildcard( + randomAlphaOfLength(10), + randomBoolean() ? randomAlphaOfLength(10) : null, + randomBoolean() ? randomAlphaOfLength(10) : null + ); + } + + @Override + protected Wildcard mutateInstance(Wildcard instance) throws IOException { + String wildcard = instance.getPattern(); + String analyzer = instance.getAnalyzer(); + String useField = instance.getUseField(); + switch (between(0, 2)) { + case 0: + wildcard += "a"; + break; + case 1: + analyzer = randomAlphaOfLength(5); + break; + case 2: + useField = useField == null ? randomAlphaOfLength(5) : null; + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + return new Wildcard(wildcard, analyzer, useField); + } + + @Override + protected Writeable.Reader instanceReader() { + return Wildcard::new; + } + + @Override + protected Wildcard doParseInstance(XContentParser parser) throws IOException { + if (parser.nextToken() == XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + Wildcard wildcard = (Wildcard) IntervalsSourceProvider.fromXContent(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + return wildcard; + } +} From 65f7a011fb081b2ad99f074907355f8079875490 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 18 Dec 2019 16:56:15 +0000 Subject: [PATCH 252/686] [ML] Refresh state index before completing data frame analytics job (#50322) In order to ensure any persisted model state is searchable by the moment the job reports itself as `stopped`, we need to refresh the state index before completing. This should fix the occasional failures we see in #50168 and #50313 where the model state appears missing. Closes #50168 Closes #50313 --- .../dataframe/process/AnalyticsProcessManager.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java index ce981ad17a98a..30ecb71f4c058 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/process/AnalyticsProcessManager.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.admin.indices.refresh.RefreshAction; import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.Client; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; @@ -159,6 +160,7 @@ private void processData(DataFrameAnalyticsTask task, ProcessContext processCont processContext.setFailureReason(resultProcessor.getFailure()); refreshDest(config); + refreshStateIndex(config.getId()); LOGGER.info("[{}] Result processor has completed", config.getId()); } catch (Exception e) { if (task.isStopping()) { @@ -288,6 +290,17 @@ private void refreshDest(DataFrameAnalyticsConfig config) { () -> client.execute(RefreshAction.INSTANCE, new RefreshRequest(config.getDest().getIndex())).actionGet()); } + private void refreshStateIndex(String jobId) { + String indexName = AnomalyDetectorsIndex.jobStateIndexPattern(); + LOGGER.debug("[{}] Refresh index {}", jobId, indexName); + + RefreshRequest refreshRequest = new RefreshRequest(indexName); + refreshRequest.indicesOptions(IndicesOptions.lenientExpandOpen()); + try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(ML_ORIGIN)) { + client.admin().indices().refresh(refreshRequest).actionGet(); + } + } + private void closeProcess(DataFrameAnalyticsTask task) { String configId = task.getParams().getId(); LOGGER.info("[{}] Closing process", configId); From 957d883c5d47c6317de98877b8de4e8989e30843 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Wed, 18 Dec 2019 12:17:41 -0500 Subject: [PATCH 253/686] [DOCS] Clarify frozen indices are read-only (#50318) The freeze index API docs state that frozen indices are blocked for write operations. While this implies frozen indices are read-only, it does not explicitly use the term "read-only", which is found in other docs, such as the force merge docs. This adds the "ready-only" term to the freeze index API docs as well as other clarification. --- docs/reference/indices/apis/freeze.asciidoc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/reference/indices/apis/freeze.asciidoc b/docs/reference/indices/apis/freeze.asciidoc index bb41de6763e32..e123e93bd30b9 100644 --- a/docs/reference/indices/apis/freeze.asciidoc +++ b/docs/reference/indices/apis/freeze.asciidoc @@ -19,9 +19,10 @@ Freezes an index. [[freeze-index-api-desc]] ==== {api-description-title} -A frozen index has almost no overhead on the cluster (except -for maintaining its metadata in memory), and is blocked for write operations. -See <> and <>. +A frozen index has almost no overhead on the cluster (except for maintaining its +metadata in memory) and is read-only. Read-only indices are blocked for write +operations, such as <> or <>. See <> and <>. IMPORTANT: Freezing an index will close the index and reopen it within the same API call. This causes primaries to not be allocated for a short amount of time From 85d69412f043042504ce8ad4888f4ec08fd821d2 Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Wed, 18 Dec 2019 10:27:16 -0700 Subject: [PATCH 254/686] Scripting: Cache script results if deterministic (#50106) Cache results from queries that use scripts if they use only deterministic API calls. Nondeterministic API calls are marked in the whitelist with the `@nondeterministic` annotation. Examples are `Math.random()` and `new Date()`. Refs: #49466 --- build.gradle | 4 +- .../NonDeterministicAnnotation.java | 29 ++ .../NonDeterministicAnnotationParser.java | 40 +++ .../annotation/WhitelistAnnotationParser.java | 3 +- .../org/elasticsearch/painless/Compiler.java | 10 +- .../painless/PainlessScriptEngine.java | 34 ++- .../elasticsearch/painless/ScriptRoot.java | 3 + .../painless/lookup/PainlessClassBinding.java | 6 +- .../painless/lookup/PainlessConstructor.java | 11 +- .../lookup/PainlessLookupBuilder.java | 53 ++-- .../painless/lookup/PainlessMethod.java | 10 +- .../painless/node/ECallLocal.java | 3 + .../painless/node/ECapturingFunctionRef.java | 2 +- .../elasticsearch/painless/node/ECast.java | 2 +- .../elasticsearch/painless/node/ENewObj.java | 3 + .../painless/node/PCallInvoke.java | 3 + .../elasticsearch/painless/node/SClass.java | 3 +- .../elasticsearch/painless/spi/java.lang.txt | 8 +- .../elasticsearch/painless/spi/java.time.txt | 10 +- .../elasticsearch/painless/spi/java.util.txt | 42 +-- .../elasticsearch/painless/FactoryTests.java | 26 ++ .../index/query/QueryShardContext.java | 2 + .../support/MultiValuesSourceFieldConfig.java | 13 +- .../AggregationTestScriptsPlugin.java | 10 + .../aggregations/bucket/DateHistogramIT.java | 26 +- .../aggregations/bucket/DateRangeIT.java | 26 +- .../bucket/DateScriptMocksPlugin.java | 8 + .../aggregations/bucket/DoubleTermsIT.java | 35 ++- .../aggregations/bucket/HistogramIT.java | 35 ++- .../aggregations/bucket/LongTermsIT.java | 35 ++- .../search/aggregations/bucket/RangeIT.java | 36 ++- .../SignificantTermsSignificanceScoreIT.java | 46 +++- .../bucket/terms/StringTermsIT.java | 37 ++- .../metrics/AvgAggregatorTests.java | 33 ++- .../aggregations/metrics/CardinalityIT.java | 37 ++- .../aggregations/metrics/ExtendedStatsIT.java | 27 +- .../metrics/HDRPercentileRanksIT.java | 29 +- .../metrics/HDRPercentilesIT.java | 27 +- .../metrics/MaxAggregatorTests.java | 31 ++- .../metrics/MedianAbsoluteDeviationIT.java | 27 +- .../metrics/MetricAggScriptPlugin.java | 13 + .../metrics/MinAggregatorTests.java | 24 +- .../metrics/ScriptedMetricIT.java | 82 +++++- .../search/aggregations/metrics/StatsIT.java | 26 +- .../search/aggregations/metrics/SumIT.java | 27 +- .../metrics/TDigestPercentileRanksIT.java | 27 +- .../metrics/TDigestPercentilesIT.java | 26 +- .../aggregations/metrics/TopHitsIT.java | 49 +++- .../aggregations/metrics/ValueCountIT.java | 28 +- .../script/MockDeterministicScript.java | 45 ++++ .../script/MockScriptEngine.java | 255 +++++++++++------- .../script/MockScriptPlugin.java | 4 +- 52 files changed, 1076 insertions(+), 355 deletions(-) create mode 100644 modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotation.java create mode 100644 modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotationParser.java create mode 100644 test/framework/src/main/java/org/elasticsearch/script/MockDeterministicScript.java diff --git a/build.gradle b/build.gradle index df1641dcc9efe..6f786b5c3beea 100644 --- a/build.gradle +++ b/build.gradle @@ -205,8 +205,8 @@ task verifyVersions { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = true -final String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */ +boolean bwc_tests_enabled = false +final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/50106" /* place a PR link here when committing bwc changes */ if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotation.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotation.java new file mode 100644 index 0000000000000..9724e42431419 --- /dev/null +++ b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotation.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package org.elasticsearch.painless.spi.annotation; + +public class NonDeterministicAnnotation { + + public static final String NAME = "nondeterministic"; + + public static final NonDeterministicAnnotation INSTANCE = new NonDeterministicAnnotation(); + + private NonDeterministicAnnotation() {} +} diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotationParser.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotationParser.java new file mode 100644 index 0000000000000..4277cf3b1d699 --- /dev/null +++ b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/NonDeterministicAnnotationParser.java @@ -0,0 +1,40 @@ +/* + * 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. + */ + +package org.elasticsearch.painless.spi.annotation; + +import java.util.Map; + +public class NonDeterministicAnnotationParser implements WhitelistAnnotationParser { + + public static final NonDeterministicAnnotationParser INSTANCE = new NonDeterministicAnnotationParser(); + + private NonDeterministicAnnotationParser() {} + + @Override + public Object parse(Map arguments) { + if (arguments.isEmpty() == false) { + throw new IllegalArgumentException( + "unexpected parameters for [@" + NonDeterministicAnnotation.NAME + "] annotation, found " + arguments + ); + } + + return NonDeterministicAnnotation.INSTANCE; + } +} diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/WhitelistAnnotationParser.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/WhitelistAnnotationParser.java index d5dcbab36f34f..ecf1c45c7602f 100644 --- a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/WhitelistAnnotationParser.java +++ b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/WhitelistAnnotationParser.java @@ -34,7 +34,8 @@ public interface WhitelistAnnotationParser { Map BASE_ANNOTATION_PARSERS = Collections.unmodifiableMap( Stream.of( new AbstractMap.SimpleEntry<>(NoImportAnnotation.NAME, NoImportAnnotationParser.INSTANCE), - new AbstractMap.SimpleEntry<>(DeprecatedAnnotation.NAME, DeprecatedAnnotationParser.INSTANCE) + new AbstractMap.SimpleEntry<>(DeprecatedAnnotation.NAME, DeprecatedAnnotationParser.INSTANCE), + new AbstractMap.SimpleEntry<>(NonDeterministicAnnotation.NAME, NonDeterministicAnnotationParser.INSTANCE) ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) ); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java index f6de8be896cb3..174936ec6ff40 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java @@ -26,7 +26,6 @@ import org.elasticsearch.painless.spi.Whitelist; import org.objectweb.asm.util.Printer; -import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; @@ -205,13 +204,14 @@ private static void addFactoryMethod(Map> additionalClasses, Cl * @param name The name of the script. * @param source The source code for the script. * @param settings The CompilerSettings to be used during the compilation. - * @return An executable script that implements both a specified interface and is a subclass of {@link PainlessScript} + * @return The ScriptRoot used to compile */ - Constructor compile(Loader loader, Set extractedVariables, String name, String source, CompilerSettings settings) { + ScriptRoot compile(Loader loader, Set extractedVariables, String name, String source, + CompilerSettings settings) { ScriptClassInfo scriptClassInfo = new ScriptClassInfo(painlessLookup, scriptClass); SClass root = Walker.buildPainlessTree(scriptClassInfo, name, source, settings, painlessLookup, null); root.extractVariables(extractedVariables); - root.analyze(painlessLookup, settings); + ScriptRoot scriptRoot = root.analyze(painlessLookup, settings); Map statics = root.write(); try { @@ -225,7 +225,7 @@ Constructor compile(Loader loader, Set extractedVariables, String nam clazz.getField(statik.getKey()).set(null, statik.getValue()); } - return clazz.getConstructors()[0]; + return scriptRoot; } catch (Exception exception) { // Catch everything to let the user know this is something caused internally. throw new IllegalStateException("An internal error occurred attempting to define the script [" + name + "].", exception); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java index 91448a4a3788e..45db4428aeced 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java @@ -143,12 +143,13 @@ public Loader run() { }); Set extractedVariables = new HashSet<>(); - compile(contextsToCompilers.get(context), loader, extractedVariables, scriptName, scriptSource, params); + ScriptRoot scriptRoot = compile(contextsToCompilers.get(context), loader, extractedVariables, scriptName, scriptSource, params); if (context.statefulFactoryClazz != null) { - return generateFactory(loader, context, extractedVariables, generateStatefulFactory(loader, context, extractedVariables)); + return generateFactory(loader, context, extractedVariables, generateStatefulFactory(loader, context, extractedVariables), + scriptRoot); } else { - return generateFactory(loader, context, extractedVariables, WriterConstants.CLASS_TYPE); + return generateFactory(loader, context, extractedVariables, WriterConstants.CLASS_TYPE, scriptRoot); } } @@ -270,6 +271,7 @@ private Type generateStatefulFactory( * @param context The {@link ScriptContext}'s semantics are used to define the factory class. * @param classType The type to be instaniated in the newFactory or newInstance method. Depends * on whether a {@link ScriptContext#statefulFactoryClazz} is specified. + * @param scriptRoot the {@link ScriptRoot} used to do the compilation * @param The factory class. * @return A factory class that will return script instances. */ @@ -277,7 +279,8 @@ private T generateFactory( Loader loader, ScriptContext context, Set extractedVariables, - Type classType + Type classType, + ScriptRoot scriptRoot ) { int classFrames = ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS; int classAccess = Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER| Opcodes.ACC_FINAL; @@ -330,8 +333,19 @@ private T generateFactory( adapter.endMethod(); writeNeedsMethods(context.factoryClazz, writer, extractedVariables); - writer.visitEnd(); + String methodName = "isResultDeterministic"; + org.objectweb.asm.commons.Method isResultDeterministic = new org.objectweb.asm.commons.Method(methodName, + MethodType.methodType(boolean.class).toMethodDescriptorString()); + + GeneratorAdapter deterAdapter = new GeneratorAdapter(Opcodes.ASM5, isResultDeterministic, + writer.visitMethod(Opcodes.ACC_PUBLIC, methodName, isResultDeterministic.getDescriptor(), null, null)); + deterAdapter.visitCode(); + deterAdapter.push(scriptRoot.deterministic); + deterAdapter.returnValue(); + deterAdapter.endMethod(); + + writer.visitEnd(); Class factory = loader.defineFactory(className.replace('/', '.'), writer.toByteArray()); try { @@ -364,19 +378,17 @@ private void writeNeedsMethods(Class clazz, ClassWriter writer, Set e } } - void compile(Compiler compiler, Loader loader, Set extractedVariables, + ScriptRoot compile(Compiler compiler, Loader loader, Set extractedVariables, String scriptName, String source, Map params) { final CompilerSettings compilerSettings = buildCompilerSettings(params); try { // Drop all permissions to actually compile the code itself. - AccessController.doPrivileged(new PrivilegedAction() { + return AccessController.doPrivileged(new PrivilegedAction() { @Override - public Void run() { + public ScriptRoot run() { String name = scriptName == null ? source : scriptName; - compiler.compile(loader, extractedVariables, name, source, compilerSettings); - - return null; + return compiler.compile(loader, extractedVariables, name, source, compilerSettings); } }, COMPILATION_CONTEXT); // Note that it is safe to catch any of the following errors since Painless is stateless. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptRoot.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptRoot.java index 72d5e50a8a6ae..775da59795b94 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptRoot.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptRoot.java @@ -38,6 +38,7 @@ public class ScriptRoot { protected final FunctionTable functionTable = new FunctionTable(); protected int syntheticCounter = 0; + protected boolean deterministic = true; public ScriptRoot(PainlessLookup painlessLookup, CompilerSettings compilerSettings, ScriptClassInfo scriptClassInfo, SClass classRoot) { this.painlessLookup = Objects.requireNonNull(painlessLookup); @@ -72,4 +73,6 @@ public FunctionTable getFunctionTable() { public String getNextSyntheticName(String prefix) { return prefix + "$synthetic$" + syntheticCounter++; } + + public void markNonDeterministic(boolean nondeterministic) { this.deterministic &= !nondeterministic; } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java index aedbc936bb1d4..971c39d3fe617 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java @@ -22,6 +22,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.List; +import java.util.Map; import java.util.Objects; public class PainlessClassBinding { @@ -31,13 +32,16 @@ public class PainlessClassBinding { public final Class returnType; public final List> typeParameters; + public final Map, Object> annotations; - PainlessClassBinding(Constructor javaConstructor, Method javaMethod, Class returnType, List> typeParameters) { + PainlessClassBinding(Constructor javaConstructor, Method javaMethod, Class returnType, List> typeParameters, + Map, Object> annotations) { this.javaConstructor = javaConstructor; this.javaMethod = javaMethod; this.returnType = returnType; this.typeParameters = typeParameters; + this.annotations = annotations; } @Override diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessConstructor.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessConstructor.java index 0f890e88b736b..b9b7a65925c24 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessConstructor.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessConstructor.java @@ -23,6 +23,7 @@ import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.util.List; +import java.util.Map; import java.util.Objects; public class PainlessConstructor { @@ -31,12 +32,15 @@ public class PainlessConstructor { public final List> typeParameters; public final MethodHandle methodHandle; public final MethodType methodType; + public final Map, Object> annotations; - PainlessConstructor(Constructor javaConstructor, List> typeParameters, MethodHandle methodHandle, MethodType methodType) { + PainlessConstructor(Constructor javaConstructor, List> typeParameters, MethodHandle methodHandle, MethodType methodType, + Map, Object> annotations) { this.javaConstructor = javaConstructor; this.typeParameters = typeParameters; this.methodHandle = methodHandle; this.methodType = methodType; + this.annotations = annotations; } @Override @@ -53,11 +57,12 @@ public boolean equals(Object object) { return Objects.equals(javaConstructor, that.javaConstructor) && Objects.equals(typeParameters, that.typeParameters) && - Objects.equals(methodType, that.methodType); + Objects.equals(methodType, that.methodType) && + Objects.equals(annotations, that.annotations); } @Override public int hashCode() { - return Objects.hash(javaConstructor, typeParameters, methodType); + return Objects.hash(javaConstructor, typeParameters, methodType, annotations); } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java index cad8bcc9a1c34..2e687b481bbc5 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java @@ -50,6 +50,7 @@ import java.security.SecureClassLoader; import java.security.cert.Certificate; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -132,7 +133,8 @@ public static PainlessLookup buildFromWhitelists(List whitelists) { for (WhitelistConstructor whitelistConstructor : whitelistClass.whitelistConstructors) { origin = whitelistConstructor.origin; painlessLookupBuilder.addPainlessConstructor( - targetCanonicalClassName, whitelistConstructor.canonicalTypeNameParameters); + targetCanonicalClassName, whitelistConstructor.canonicalTypeNameParameters, + whitelistConstructor.painlessAnnotations); } for (WhitelistMethod whitelistMethod : whitelistClass.whitelistMethods) { @@ -140,7 +142,7 @@ public static PainlessLookup buildFromWhitelists(List whitelists) { painlessLookupBuilder.addPainlessMethod( whitelist.classLoader, targetCanonicalClassName, whitelistMethod.augmentedCanonicalClassName, whitelistMethod.methodName, whitelistMethod.returnCanonicalTypeName, - whitelistMethod.canonicalTypeNameParameters); + whitelistMethod.canonicalTypeNameParameters, whitelistMethod.painlessAnnotations); } for (WhitelistField whitelistField : whitelistClass.whitelistFields) { @@ -155,14 +157,16 @@ public static PainlessLookup buildFromWhitelists(List whitelists) { painlessLookupBuilder.addImportedPainlessMethod( whitelist.classLoader, whitelistStatic.augmentedCanonicalClassName, whitelistStatic.methodName, whitelistStatic.returnCanonicalTypeName, - whitelistStatic.canonicalTypeNameParameters); + whitelistStatic.canonicalTypeNameParameters, + whitelistStatic.painlessAnnotations); } for (WhitelistClassBinding whitelistClassBinding : whitelist.whitelistClassBindings) { origin = whitelistClassBinding.origin; painlessLookupBuilder.addPainlessClassBinding( whitelist.classLoader, whitelistClassBinding.targetJavaClassName, whitelistClassBinding.methodName, - whitelistClassBinding.returnCanonicalTypeName, whitelistClassBinding.canonicalTypeNameParameters); + whitelistClassBinding.returnCanonicalTypeName, whitelistClassBinding.canonicalTypeNameParameters, + whitelistClassBinding.painlessAnnotations); } for (WhitelistInstanceBinding whitelistInstanceBinding : whitelist.whitelistInstanceBindings) { @@ -313,7 +317,8 @@ public void addPainlessClass(Class clazz, boolean importClassName) { } } - public void addPainlessConstructor(String targetCanonicalClassName, List canonicalTypeNameParameters) { + public void addPainlessConstructor(String targetCanonicalClassName, List canonicalTypeNameParameters, + Map, Object> annotations) { Objects.requireNonNull(targetCanonicalClassName); Objects.requireNonNull(canonicalTypeNameParameters); @@ -337,10 +342,10 @@ public void addPainlessConstructor(String targetCanonicalClassName, List typeParameters.add(typeParameter); } - addPainlessConstructor(targetClass, typeParameters); + addPainlessConstructor(targetClass, typeParameters, annotations); } - public void addPainlessConstructor(Class targetClass, List> typeParameters) { + public void addPainlessConstructor(Class targetClass, List> typeParameters, Map, Object> annotations) { Objects.requireNonNull(targetClass); Objects.requireNonNull(typeParameters); @@ -390,7 +395,8 @@ public void addPainlessConstructor(Class targetClass, List> typePara String painlessConstructorKey = buildPainlessConstructorKey(typeParametersSize); PainlessConstructor existingPainlessConstructor = painlessClassBuilder.constructors.get(painlessConstructorKey); - PainlessConstructor newPainlessConstructor = new PainlessConstructor(javaConstructor, typeParameters, methodHandle, methodType); + PainlessConstructor newPainlessConstructor = new PainlessConstructor(javaConstructor, typeParameters, methodHandle, methodType, + annotations); if (existingPainlessConstructor == null) { newPainlessConstructor = painlessConstructorCache.computeIfAbsent(newPainlessConstructor, key -> key); @@ -403,7 +409,8 @@ public void addPainlessConstructor(Class targetClass, List> typePara } public void addPainlessMethod(ClassLoader classLoader, String targetCanonicalClassName, String augmentedCanonicalClassName, - String methodName, String returnCanonicalTypeName, List canonicalTypeNameParameters) { + String methodName, String returnCanonicalTypeName, List canonicalTypeNameParameters, + Map, Object> annotations) { Objects.requireNonNull(classLoader); Objects.requireNonNull(targetCanonicalClassName); @@ -449,11 +456,11 @@ public void addPainlessMethod(ClassLoader classLoader, String targetCanonicalCla "[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]"); } - addPainlessMethod(targetClass, augmentedClass, methodName, returnType, typeParameters); + addPainlessMethod(targetClass, augmentedClass, methodName, returnType, typeParameters, annotations); } public void addPainlessMethod(Class targetClass, Class augmentedClass, - String methodName, Class returnType, List> typeParameters) { + String methodName, Class returnType, List> typeParameters, Map, Object> annotations) { Objects.requireNonNull(targetClass); Objects.requireNonNull(methodName); @@ -562,7 +569,7 @@ public void addPainlessMethod(Class targetClass, Class augmentedClass, painlessClassBuilder.staticMethods.get(painlessMethodKey) : painlessClassBuilder.methods.get(painlessMethodKey); PainlessMethod newPainlessMethod = - new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType); + new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType, annotations); if (existingPainlessMethod == null) { newPainlessMethod = painlessMethodCache.computeIfAbsent(newPainlessMethod, key -> key); @@ -708,7 +715,8 @@ public void addPainlessField(Class targetClass, String fieldName, Class ty } public void addImportedPainlessMethod(ClassLoader classLoader, String targetJavaClassName, - String methodName, String returnCanonicalTypeName, List canonicalTypeNameParameters) { + String methodName, String returnCanonicalTypeName, List canonicalTypeNameParameters, + Map, Object> annotations) { Objects.requireNonNull(classLoader); Objects.requireNonNull(targetJavaClassName); @@ -751,10 +759,11 @@ public void addImportedPainlessMethod(ClassLoader classLoader, String targetJava "[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]"); } - addImportedPainlessMethod(targetClass, methodName, returnType, typeParameters); + addImportedPainlessMethod(targetClass, methodName, returnType, typeParameters, annotations); } - public void addImportedPainlessMethod(Class targetClass, String methodName, Class returnType, List> typeParameters) { + public void addImportedPainlessMethod(Class targetClass, String methodName, Class returnType, List> typeParameters, + Map, Object> annotations) { Objects.requireNonNull(targetClass); Objects.requireNonNull(methodName); Objects.requireNonNull(returnType); @@ -841,7 +850,7 @@ public void addImportedPainlessMethod(Class targetClass, String methodName, C PainlessMethod existingImportedPainlessMethod = painlessMethodKeysToImportedPainlessMethods.get(painlessMethodKey); PainlessMethod newImportedPainlessMethod = - new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType); + new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType, annotations); if (existingImportedPainlessMethod == null) { newImportedPainlessMethod = painlessMethodCache.computeIfAbsent(newImportedPainlessMethod, key -> key); @@ -859,7 +868,8 @@ public void addImportedPainlessMethod(Class targetClass, String methodName, C } public void addPainlessClassBinding(ClassLoader classLoader, String targetJavaClassName, - String methodName, String returnCanonicalTypeName, List canonicalTypeNameParameters) { + String methodName, String returnCanonicalTypeName, List canonicalTypeNameParameters, + Map, Object> annotations) { Objects.requireNonNull(classLoader); Objects.requireNonNull(targetJavaClassName); @@ -896,10 +906,11 @@ public void addPainlessClassBinding(ClassLoader classLoader, String targetJavaCl "[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]"); } - addPainlessClassBinding(targetClass, methodName, returnType, typeParameters); + addPainlessClassBinding(targetClass, methodName, returnType, typeParameters, annotations); } - public void addPainlessClassBinding(Class targetClass, String methodName, Class returnType, List> typeParameters) { + public void addPainlessClassBinding(Class targetClass, String methodName, Class returnType, List> typeParameters, + Map, Object> annotations) { Objects.requireNonNull(targetClass); Objects.requireNonNull(methodName); Objects.requireNonNull(returnType); @@ -1036,7 +1047,7 @@ public void addPainlessClassBinding(Class targetClass, String methodName, Cla PainlessClassBinding existingPainlessClassBinding = painlessMethodKeysToPainlessClassBindings.get(painlessMethodKey); PainlessClassBinding newPainlessClassBinding = - new PainlessClassBinding(javaConstructor, javaMethod, returnType, typeParameters); + new PainlessClassBinding(javaConstructor, javaMethod, returnType, typeParameters, annotations); if (existingPainlessClassBinding == null) { newPainlessClassBinding = painlessClassBindingCache.computeIfAbsent(newPainlessClassBinding, key -> key); @@ -1444,7 +1455,7 @@ public BridgeLoader run() { painlessMethod.javaMethod.getName(), bridgeTypeParameters.toArray(new Class[0])); MethodHandle bridgeHandle = MethodHandles.publicLookup().in(bridgeClass).unreflect(bridgeClass.getMethods()[0]); bridgePainlessMethod = new PainlessMethod(bridgeMethod, bridgeClass, - painlessMethod.returnType, bridgeTypeParameters, bridgeHandle, bridgeMethodType); + painlessMethod.returnType, bridgeTypeParameters, bridgeHandle, bridgeMethodType, Collections.emptyMap()); painlessClassBuilder.runtimeMethods.put(painlessMethodKey.intern(), bridgePainlessMethod); painlessBridgeCache.put(painlessMethod, bridgePainlessMethod); } catch (Exception exception) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessMethod.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessMethod.java index 358449c36f3e5..6e84f5b28e77e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessMethod.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessMethod.java @@ -23,6 +23,7 @@ import java.lang.invoke.MethodType; import java.lang.reflect.Method; import java.util.List; +import java.util.Map; import java.util.Objects; public class PainlessMethod { @@ -33,9 +34,10 @@ public class PainlessMethod { public final List> typeParameters; public final MethodHandle methodHandle; public final MethodType methodType; + public final Map, Object> annotations; public PainlessMethod(Method javaMethod, Class targetClass, Class returnType, List> typeParameters, - MethodHandle methodHandle, MethodType methodType) { + MethodHandle methodHandle, MethodType methodType, Map, Object> annotations) { this.javaMethod = javaMethod; this.targetClass = targetClass; @@ -43,6 +45,7 @@ public PainlessMethod(Method javaMethod, Class targetClass, Class returnTy this.typeParameters = List.copyOf(typeParameters); this.methodHandle = methodHandle; this.methodType = methodType; + this.annotations = annotations; } @Override @@ -61,11 +64,12 @@ public boolean equals(Object object) { Objects.equals(targetClass, that.targetClass) && Objects.equals(returnType, that.returnType) && Objects.equals(typeParameters, that.typeParameters) && - Objects.equals(methodType, that.methodType); + Objects.equals(methodType, that.methodType) && + Objects.equals(annotations, that.annotations); } @Override public int hashCode() { - return Objects.hash(javaMethod, targetClass, returnType, typeParameters, methodType); + return Objects.hash(javaMethod, targetClass, returnType, typeParameters, methodType, annotations); } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java index e386f94d01b69..b96782f14855f 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java @@ -28,6 +28,7 @@ import org.elasticsearch.painless.lookup.PainlessClassBinding; import org.elasticsearch.painless.lookup.PainlessInstanceBinding; import org.elasticsearch.painless.lookup.PainlessMethod; +import org.elasticsearch.painless.spi.annotation.NonDeterministicAnnotation; import org.elasticsearch.painless.symbol.FunctionTable; import org.objectweb.asm.Label; import org.objectweb.asm.Type; @@ -127,9 +128,11 @@ void analyze(ScriptRoot scriptRoot, Locals locals) { typeParameters = new ArrayList<>(localFunction.getTypeParameters()); actual = localFunction.getReturnType(); } else if (importedMethod != null) { + scriptRoot.markNonDeterministic(importedMethod.annotations.containsKey(NonDeterministicAnnotation.class)); typeParameters = new ArrayList<>(importedMethod.typeParameters); actual = importedMethod.returnType; } else if (classBinding != null) { + scriptRoot.markNonDeterministic(classBinding.annotations.containsKey(NonDeterministicAnnotation.class)); typeParameters = new ArrayList<>(classBinding.typeParameters); actual = classBinding.returnType; bindingName = scriptRoot.getNextSyntheticName("class_binding"); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java index 00f2283472e49..dd76ddc6d0974 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java @@ -37,7 +37,7 @@ import java.util.Set; /** - * Represents a capturing function reference. + * Represents a capturing function reference. For member functions that require a this reference, ie not static. */ public final class ECapturingFunctionRef extends AExpression implements ILambda { private final String variable; diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECast.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECast.java index d33f37fb6049b..0e50368bf7c3b 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECast.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECast.java @@ -32,7 +32,7 @@ import java.util.Set; /** - * Represents a cast that is inserted into the tree replacing other casts. (Internal only.) + * Represents a cast that is inserted into the tree replacing other casts. (Internal only.) Casts are inserted during semantic checking. */ final class ECast extends AExpression { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java index 5f17cd9696473..0bc5b751f8831 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java @@ -27,6 +27,7 @@ import org.elasticsearch.painless.ScriptRoot; import org.elasticsearch.painless.lookup.PainlessConstructor; import org.elasticsearch.painless.lookup.PainlessLookupUtility; +import org.elasticsearch.painless.spi.annotation.NonDeterministicAnnotation; import org.objectweb.asm.Type; import org.objectweb.asm.commons.Method; @@ -75,6 +76,8 @@ void analyze(ScriptRoot scriptRoot, Locals locals) { "constructor [" + typeToCanonicalTypeName(actual) + ", /" + arguments.size() + "] not found")); } + scriptRoot.markNonDeterministic(constructor.annotations.containsKey(NonDeterministicAnnotation.class)); + Class[] types = new Class[constructor.typeParameters.size()]; constructor.typeParameters.toArray(types); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java index 47bd54c288640..e71f70d632579 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java @@ -27,6 +27,7 @@ import org.elasticsearch.painless.ScriptRoot; import org.elasticsearch.painless.lookup.PainlessMethod; import org.elasticsearch.painless.lookup.def; +import org.elasticsearch.painless.spi.annotation.NonDeterministicAnnotation; import java.util.List; import java.util.Objects; @@ -79,6 +80,8 @@ void analyze(ScriptRoot scriptRoot, Locals locals) { "method [" + typeToCanonicalTypeName(prefix.actual) + ", " + name + "/" + arguments.size() + "] not found")); } + scriptRoot.markNonDeterministic(method.annotations.containsKey(NonDeterministicAnnotation.class)); + sub = new PSubCallInvoke(location, method, prefix.actual, arguments); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SClass.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SClass.java index 356f56d7cf491..7145a4a9e33ad 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SClass.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SClass.java @@ -130,7 +130,7 @@ public void extractVariables(Set variables) { extractedVariables.addAll(variables); } - public void analyze(PainlessLookup painlessLookup, CompilerSettings settings) { + public ScriptRoot analyze(PainlessLookup painlessLookup, CompilerSettings settings) { this.settings = settings; table = new ScriptRoot(painlessLookup, settings, scriptClassInfo, this); @@ -148,6 +148,7 @@ public void analyze(PainlessLookup painlessLookup, CompilerSettings settings) { Locals locals = Locals.newProgramScope(); analyze(table, locals); + return table; } @Override diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.lang.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.lang.txt index 63ed6d41c676d..b900f62d7fe98 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.lang.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.lang.txt @@ -636,7 +636,7 @@ class java.lang.Math { double nextDown(double) double nextUp(double) double pow(double,double) - double random() + double random() @nondeterministic double rint(double) long round(double) double scalb(double,int) @@ -729,7 +729,7 @@ class java.lang.StrictMath { double nextDown(double) double nextUp(double) double pow(double,double) - double random() + double random() @nondeterministic double rint(double) long round(double) double scalb(double,int) @@ -844,8 +844,8 @@ class java.lang.StringBuilder { class java.lang.System { void arraycopy(Object,int,Object,int,int) - long currentTimeMillis() - long nanoTime() + long currentTimeMillis() @nondeterministic + long nanoTime() @nondeterministic } # Thread: skipped diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.time.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.time.txt index 0cedc849a6838..38c6e8a4f575e 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.time.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.time.txt @@ -26,11 +26,11 @@ class java.time.Clock { Clock fixed(Instant,ZoneId) - ZoneId getZone() - Instant instant() - long millis() - Clock offset(Clock,Duration) - Clock tick(Clock,Duration) + ZoneId getZone() @nondeterministic + Instant instant() @nondeterministic + long millis() @nondeterministic + Clock offset(Clock,Duration) @nondeterministic + Clock tick(Clock,Duration) @nondeterministic } class java.time.Duration { diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.util.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.util.txt index e7cc5bb7db463..12da1f679422c 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.util.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/java.util.txt @@ -490,9 +490,9 @@ class java.util.Calendar { Map getDisplayNames(int,int,Locale) int getFirstDayOfWeek() int getGreatestMinimum(int) - Calendar getInstance() - Calendar getInstance(TimeZone) - Calendar getInstance(TimeZone,Locale) + Calendar getInstance() @nondeterministic + Calendar getInstance(TimeZone) @nondeterministic + Calendar getInstance(TimeZone,Locale) @nondeterministic int getLeastMaximum(int) int getMaximum(int) int getMinimalDaysInFirstWeek() @@ -574,7 +574,7 @@ class java.util.Collections { Comparator reverseOrder() Comparator reverseOrder(Comparator) void rotate(List,int) - void shuffle(List) + void shuffle(List) @nondeterministic void shuffle(List,Random) Set singleton(def) List singletonList(def) @@ -605,7 +605,7 @@ class java.util.Currency { } class java.util.Date { - () + () @nondeterministic (long) boolean after(Date) boolean before(Date) @@ -910,22 +910,22 @@ class java.util.PriorityQueue { } class java.util.Random { - () + () @nondeterministic (long) - DoubleStream doubles(long) - DoubleStream doubles(long,double,double) - IntStream ints(long) - IntStream ints(long,int,int) - LongStream longs(long) - LongStream longs(long,long,long) - boolean nextBoolean() - void nextBytes(byte[]) - double nextDouble() - float nextFloat() - double nextGaussian() - int nextInt() - int nextInt(int) - long nextLong() + DoubleStream doubles(long) @nondeterministic + DoubleStream doubles(long,double,double) @nondeterministic + IntStream ints(long) @nondeterministic + IntStream ints(long,int,int) @nondeterministic + LongStream longs(long) @nondeterministic + LongStream longs(long,long,long) @nondeterministic + boolean nextBoolean() @nondeterministic + void nextBytes(byte[]) @nondeterministic + double nextDouble() @nondeterministic + float nextFloat() @nondeterministic + double nextGaussian() @nondeterministic + int nextInt() @nondeterministic + int nextInt(int) @nondeterministic + long nextLong() @nondeterministic void setSeed(long) } @@ -1031,7 +1031,7 @@ class java.util.UUID { UUID fromString(String) long getLeastSignificantBits() long getMostSignificantBits() - UUID randomUUID() + UUID randomUUID() @nondeterministic UUID nameUUIDFromBytes(byte[]) long node() long timestamp() diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java index 1d3fd829d2512..33645e24c0b3c 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java @@ -162,6 +162,32 @@ public void testFactory() { assertEquals(false, factory.needsNothing()); } + public void testDeterministic() { + FactoryTestScript.Factory factory = + scriptEngine.compile("deterministic_test", "Integer.parseInt('123')", + FactoryTestScript.CONTEXT, Collections.emptyMap()); + assertTrue(factory.isResultDeterministic()); + assertEquals(123, factory.newInstance(Collections.emptyMap()).execute(0)); + } + + public void testNotDeterministic() { + FactoryTestScript.Factory factory = + scriptEngine.compile("not_deterministic_test", "Math.random()", + FactoryTestScript.CONTEXT, Collections.emptyMap()); + assertFalse(factory.isResultDeterministic()); + Double d = (Double)factory.newInstance(Collections.emptyMap()).execute(0); + assertTrue(d >= 0.0 && d <= 1.0); + } + + public void testMixedDeterministicIsNotDeterministic() { + FactoryTestScript.Factory factory = + scriptEngine.compile("not_deterministic_test", "Integer.parseInt('123') + Math.random()", + FactoryTestScript.CONTEXT, Collections.emptyMap()); + assertFalse(factory.isResultDeterministic()); + Double d = (Double)factory.newInstance(Collections.emptyMap()).execute(0); + assertTrue(d >= 123.0 && d <= 124.0); + } + public abstract static class EmptyTestScript { public static final String[] PARAMETERS = {}; public abstract Object execute(); diff --git a/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java b/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java index 88a8351790c0a..56161d8f2fa34 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java @@ -325,6 +325,8 @@ public Index index() { /** Compile script using script service */ public FactoryType compile(Script script, ScriptContext context) { FactoryType factory = scriptService.compile(script, context); + // TODO(stu): can check `factory instanceof ScriptFactory && ((ScriptFactory) factory).isResultDeterministic() == false` + // to avoid being so intrusive if (factory.isResultDeterministic() == false) { failIfFrozen(); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfig.java b/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfig.java index 54baba9b6b7e5..a7412f10ceccb 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfig.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfig.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.support; +import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -80,7 +81,11 @@ private MultiValuesSourceFieldConfig(String fieldName, Object missing, Script sc } public MultiValuesSourceFieldConfig(StreamInput in) throws IOException { - this.fieldName = in.readString(); + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { + this.fieldName = in.readOptionalString(); + } else { + this.fieldName = in.readString(); + } this.missing = in.readGenericValue(); this.script = in.readOptionalWriteable(Script::new); this.timeZone = in.readOptionalZoneId(); @@ -104,7 +109,11 @@ public String getFieldName() { @Override public void writeTo(StreamOutput out) throws IOException { - out.writeString(fieldName); + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { + out.writeOptionalString(fieldName); + } else { + out.writeString(fieldName); + } out.writeGenericValue(missing); out.writeOptionalWriteable(script); out.writeOptionalZoneId(timeZone); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/AggregationTestScriptsPlugin.java b/server/src/test/java/org/elasticsearch/search/aggregations/AggregationTestScriptsPlugin.java index 5d71176e79820..873dd89bdcfd1 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/AggregationTestScriptsPlugin.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/AggregationTestScriptsPlugin.java @@ -23,6 +23,7 @@ import org.elasticsearch.script.MockScriptPlugin; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.test.ESTestCase; import java.util.HashMap; import java.util.Map; @@ -116,4 +117,13 @@ protected Map, Object>> pluginScripts() { return scripts; } + + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("Math.random()", vars -> ESTestCase.randomDouble()); + + return scripts; + } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java index dbf527aa6811a..0dceff9d0f0d1 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java @@ -1466,10 +1466,10 @@ public void testDSTEndTransition() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=date") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -1484,10 +1484,21 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached Map params = new HashMap<>(); params.put("fieldname", "d"); SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(dateHistogram("histo").field("d") + .script(new Script(ScriptType.INLINE, "mockscript", DateScriptMocksPlugin.CURRENT_DATE, params)) + .dateHistogramInterval(DateHistogramInterval.MONTH)).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(0L)); + + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(dateHistogram("histo").field("d") .script(new Script(ScriptType.INLINE, "mockscript", DateScriptMocksPlugin.LONG_PLUS_ONE_MONTH, params)) .dateHistogramInterval(DateHistogramInterval.MONTH)).get(); assertSearchResponse(r); @@ -1495,10 +1506,9 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getMissCount(), equalTo(0L)); + .getMissCount(), equalTo(1L)); - // To make sure that the cache is working test that a request not using - // a script is cached + // Ensure that non-scripted requests are cached as normal r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation(dateHistogram("histo").field("d").dateHistogramInterval(DateHistogramInterval.MONTH)).get(); assertSearchResponse(r); @@ -1506,7 +1516,7 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getMissCount(), equalTo(1L)); + .getMissCount(), equalTo(2L)); } public void testSingleValuedFieldOrderedBySingleValueSubAggregationAscAndKeyDesc() throws Exception { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateRangeIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateRangeIT.java index 820ea3786508c..4b6bdf96b891e 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateRangeIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateRangeIT.java @@ -884,10 +884,10 @@ public void testNoRangesInQuery() { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "date", "type=date") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -903,11 +903,11 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached Map params = new HashMap<>(); params.put("fieldname", "date"); SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(dateRange("foo").field("date") - .script(new Script(ScriptType.INLINE, "mockscript", DateScriptMocksPlugin.DOUBLE_PLUS_ONE_MONTH, params)) + .script(new Script(ScriptType.INLINE, "mockscript", DateScriptMocksPlugin.CURRENT_DATE, params)) .addRange(ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), ZonedDateTime.of(2013, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC))) .get(); @@ -918,9 +918,9 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached + // Test that a request using a deterministic script gets cached r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(dateRange("foo").field("date") + .script(new Script(ScriptType.INLINE, "mockscript", DateScriptMocksPlugin.DOUBLE_PLUS_ONE_MONTH, params)) .addRange(ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), ZonedDateTime.of(2013, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC))) .get(); @@ -930,6 +930,18 @@ public void testDontCacheScripts() throws Exception { .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(dateRange("foo").field("date") + .addRange(ZonedDateTime.of(2012, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2013, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC))) + .get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } /** diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateScriptMocksPlugin.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateScriptMocksPlugin.java index 1398961ced8af..07f7adf5e8446 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateScriptMocksPlugin.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateScriptMocksPlugin.java @@ -38,6 +38,7 @@ public class DateScriptMocksPlugin extends MockScriptPlugin { static final String EXTRACT_FIELD = "extract_field"; static final String DOUBLE_PLUS_ONE_MONTH = "double_date_plus_1_month"; static final String LONG_PLUS_ONE_MONTH = "long_date_plus_1_month"; + static final String CURRENT_DATE = "current_date"; @Override public Map, Object>> pluginScripts() { @@ -53,4 +54,11 @@ public Map, Object>> pluginScripts() { new DateTime((long) params.get("_value"), DateTimeZone.UTC).plusMonths(1).getMillis()); return scripts; } + + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + scripts.put(CURRENT_DATE, params -> new DateTime().getMillis()); + return scripts; + } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DoubleTermsIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DoubleTermsIT.java index e99931c62102b..c3a8c9b18d390 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DoubleTermsIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DoubleTermsIT.java @@ -112,6 +112,15 @@ protected Map, Object>> pluginScripts() { return scripts; } + + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("Math.random()", vars -> DoubleTermsIT.randomDouble()); + + return scripts; + } } private static final int NUM_DOCS = 5; // TODO: randomize the size? @@ -917,10 +926,10 @@ public void testOtherDocCount() { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=float") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -933,10 +942,10 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation( terms("terms").field("d").script( - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap()))).get(); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() @@ -944,14 +953,24 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation( + terms("terms").field("d").script( + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap()))).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(terms("terms").field("d")).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getMissCount(), equalTo(1L)); + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/HistogramIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/HistogramIT.java index f86aef40834af..f8c5390de54cd 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/HistogramIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/HistogramIT.java @@ -110,6 +110,15 @@ protected Map, Object>> pluginScripts() { return scripts; } + + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("Math.random()", vars -> HistogramIT.randomDouble()); + + return scripts; + } } @Override @@ -1102,10 +1111,10 @@ public void testDecimalIntervalAndOffset() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=float") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -1118,9 +1127,10 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(histogram("histo").field("d") - .script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", emptyMap())).interval(0.7).offset(0.05)).get(); + .script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", emptyMap())).interval(0.7).offset(0.05)) + .get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() @@ -1128,8 +1138,17 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(histogram("histo").field("d") + .script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", emptyMap())).interval(0.7).offset(0.05)).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(histogram("histo").field("d").interval(0.7).offset(0.05)) .get(); assertSearchResponse(r); @@ -1137,7 +1156,7 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getMissCount(), equalTo(1L)); + .getMissCount(), equalTo(2L)); } public void testSingleValuedFieldOrderedBySingleValueSubAggregationAscAndKeyDesc() throws Exception { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/LongTermsIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/LongTermsIT.java index 1c0a5de546766..2d5d7d33aefdf 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/LongTermsIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/LongTermsIT.java @@ -99,6 +99,15 @@ protected Map, Object>> pluginScripts() { return scripts; } + + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("Math.random()", vars -> LongTermsIT.randomDouble()); + + return scripts; + } } private static final int NUM_DOCS = 5; // TODO randomize the size? @@ -894,10 +903,10 @@ public void testOtherDocCount() { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -910,10 +919,10 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation( terms("terms").field("d").script( - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap()))).get(); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() @@ -921,14 +930,24 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(terms("terms").field("d")).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation( + terms("terms").field("d").script( + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(terms("terms").field("d")).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/RangeIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/RangeIT.java index f7a1ce30d1ed7..32e8516910ea8 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/RangeIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/RangeIT.java @@ -92,6 +92,15 @@ protected Map, Object>> pluginScripts() { return scripts; } + + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("Math.random()", vars -> RangeIT.randomDouble()); + + return scripts; + } } @Override @@ -945,10 +954,10 @@ public void testEmptyAggregation() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "i", "type=integer") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -962,12 +971,12 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached Map params = new HashMap<>(); params.put("fieldname", "date"); SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation( range("foo").field("i").script( - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap())).addRange(0, 10)) + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap())).addRange(0, 10)) .get(); assertSearchResponse(r); @@ -976,15 +985,26 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(range("foo").field("i").addRange(0, 10)).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation( + range("foo").field("i").script( + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value + 1", Collections.emptyMap())).addRange(0, 10)) + .get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(range("foo").field("i").addRange(0, 10)).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } public void testFieldAlias() { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java index 2f231083c684d..80b67fe850d8d 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java @@ -193,6 +193,15 @@ public Map, Object>> pluginScripts() { return scripts; } + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("Math.random()", vars -> SignificantTermsSignificanceScoreIT.randomDouble()); + + return scripts; + } + private static long longValue(Object value) { return ((ScriptHeuristic.LongAccessor) value).longValue(); } @@ -678,10 +687,10 @@ public void testReduceFromSeveralShards() throws IOException, ExecutionException } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -694,8 +703,10 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached - ScriptHeuristic scriptHeuristic = getScriptSignificanceHeuristic(); + // Test that a request using a nondeterministic script does not get cached + ScriptHeuristic scriptHeuristic = new ScriptHeuristic( + new Script(ScriptType.INLINE, "mockscript", "Math.random()", Collections.emptyMap()) + ); boolean useSigText = randomBoolean(); SearchResponse r; if (useSigText) { @@ -712,12 +723,15 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached + // Test that a request using a deterministic script gets cached + scriptHeuristic = getScriptSignificanceHeuristic(); + useSigText = randomBoolean(); if (useSigText) { - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(significantText("foo", "s")).get(); + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(significantText("foo", "s").significanceHeuristic(scriptHeuristic)).get(); } else { - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(significantTerms("foo").field("s")).get(); + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(significantTerms("foo").field("s").significanceHeuristic(scriptHeuristic)).get(); } assertSearchResponse(r); @@ -725,8 +739,18 @@ public void testDontCacheScripts() throws Exception { .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); - } - + // Ensure that non-scripted requests are cached as normal + if (useSigText) { + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(significantText("foo", "s")).get(); + } else { + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(significantTerms("foo").field("s")).get(); + } + assertSearchResponse(r); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); + } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsIT.java index a748eb95bbf58..dda55c9e8f9df 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsIT.java @@ -119,6 +119,15 @@ protected Map, Object>> pluginScripts() { return scripts; } + + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("Math.random()", vars -> StringTermsIT.randomDouble()); + + return scripts; + } } @Override @@ -1115,10 +1124,10 @@ public void testOtherDocCount() { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=keyword") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -1131,11 +1140,11 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation( terms("terms").field("d").script( - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "'foo_' + _value", Collections.emptyMap()))) + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap()))) .get(); assertSearchResponse(r); @@ -1144,14 +1153,26 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(terms("terms").field("d")).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation( + terms("terms").field("d").script( + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "'foo_' + _value", Collections.emptyMap()))) + .get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(terms("terms").field("d")).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/AvgAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/AvgAggregatorTests.java index 4dfdfd2fd91ca..46f08d786a856 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/AvgAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/AvgAggregatorTests.java @@ -83,6 +83,9 @@ public class AvgAggregatorTests extends AggregatorTestCase { /** Script to return the {@code _value} provided by aggs framework. */ public static final String VALUE_SCRIPT = "_value"; + /** Script to return a random double */ + public static final String RANDOM_SCRIPT = "Math.random()"; + @Override protected ScriptService getMockScriptService() { Map, Object>> scripts = new HashMap<>(); @@ -115,8 +118,12 @@ protected ScriptService getMockScriptService() { return ((Number) vars.get("_value")).doubleValue() + inc; }); + Map, Object>> nonDeterministicScripts = new HashMap<>(); + nonDeterministicScripts.put(RANDOM_SCRIPT, vars -> AvgAggregatorTests.randomDouble()); + MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, scripts, + nonDeterministicScripts, Collections.emptyMap()); Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); @@ -638,9 +645,10 @@ public void testCacheAggregation() throws IOException { } /** - * Make sure that an aggregation using a script does not get cached. + * Make sure that an aggregation using a deterministic script does gets cached while + * one using a nondeterministic script does not. */ - public void testDontCacheScripts() throws IOException { + public void testScriptCaching() throws IOException { Directory directory = newDirectory(); RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); final int numDocs = 10; @@ -675,7 +683,26 @@ public void testDontCacheScripts() throws IOException { assertEquals("avg", avg.getName()); assertTrue(AggregationInspectionHelper.hasValue(avg)); - // Test that an aggregation using a script does not get cached + // Test that an aggregation using a deterministic script gets cached + assertTrue(aggregator.context().getQueryShardContext().isCacheable()); + + aggregationBuilder = new AvgAggregationBuilder("avg") + .field("value") + .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, RANDOM_SCRIPT, Collections.emptyMap())); + + aggregator = createAggregator(aggregationBuilder, indexSearcher, fieldType); + aggregator.preCollection(); + indexSearcher.search(new MatchAllDocsQuery(), aggregator); + aggregator.postCollection(); + + avg = (InternalAvg) aggregator.buildAggregation(0L); + + assertTrue(avg.getValue() >= 0.0); + assertTrue(avg.getValue() <= 1.0); + assertEquals("avg", avg.getName()); + assertTrue(AggregationInspectionHelper.hasValue(avg)); + + // Test that an aggregation using a nondeterministic script does not get cached assertFalse(aggregator.context().getQueryShardContext().isCacheable()); multiReader.close(); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/CardinalityIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/CardinalityIT.java index 347ec478e19e0..b290d44a3e85d 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/CardinalityIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/CardinalityIT.java @@ -90,6 +90,15 @@ protected Map, Object>> pluginScripts() { return scripts; } + + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("Math.random()", vars -> CardinalityIT.randomDouble()); + + return scripts; + } } @Override @@ -449,10 +458,10 @@ public void testAsSubAgg() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -465,10 +474,11 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation( - cardinality("foo").field("d").script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value", emptyMap()))) + cardinality("foo").field("d").script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", + emptyMap()))) .get(); assertSearchResponse(r); @@ -477,14 +487,25 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(cardinality("foo").field("d")).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation( + cardinality("foo").field("d").script(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_value", emptyMap()))) + .get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(cardinality("foo").field("d")).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ExtendedStatsIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ExtendedStatsIT.java index 80a26ee9ebb2f..58967969d54a0 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ExtendedStatsIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ExtendedStatsIT.java @@ -639,10 +639,10 @@ private void checkUpperLowerBounds(ExtendedStats stats, double sigma) { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -655,10 +655,10 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation(extendedStats("foo").field("d") - .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value + 1", Collections.emptyMap()))) + .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", Collections.emptyMap()))) .get(); assertSearchResponse(r); @@ -667,15 +667,26 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(extendedStats("foo").field("d")).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(extendedStats("foo").field("d") + .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value + 1", Collections.emptyMap()))) + .get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(extendedStats("foo").field("d")).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentileRanksIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentileRanksIT.java index 7b7aef1b90b74..ae578208ae01b 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentileRanksIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentileRanksIT.java @@ -558,10 +558,10 @@ public void testOrderByEmptyAggregation() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -574,12 +574,12 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client() .prepareSearch("cache_test_idx").setSize(0) .addAggregation(percentileRanks("foo", new double[]{50.0}) .method(PercentilesMethod.HDR).field("d") - .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))) + .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", emptyMap()))) .get(); assertSearchResponse(r); @@ -588,8 +588,21 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached + // Test that a request using a deterministic script gets cached + r = client() + .prepareSearch("cache_test_idx").setSize(0) + .addAggregation(percentileRanks("foo", new double[]{50.0}) + .method(PercentilesMethod.HDR).field("d") + .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))) + .get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation(percentileRanks("foo", new double[]{50.0}).method(PercentilesMethod.HDR).field("d")).get(); assertSearchResponse(r); @@ -597,7 +610,7 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getMissCount(), equalTo(1L)); + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentilesIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentilesIT.java index 79f8ce4f829a6..05ffe4ddc4638 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentilesIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/HDRPercentilesIT.java @@ -523,10 +523,10 @@ public void testOrderByEmptyAggregation() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -539,10 +539,10 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation(percentiles("foo").method(PercentilesMethod.HDR).field("d").percentiles(50.0) - .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))) + .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", emptyMap()))) .get(); assertSearchResponse(r); @@ -551,16 +551,27 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached + // Test that a request using a deterministic script gets cached r = client().prepareSearch("cache_test_idx").setSize(0) - .addAggregation(percentiles("foo").method(PercentilesMethod.HDR).field("d").percentiles(50.0)).get(); + .addAggregation(percentiles("foo").method(PercentilesMethod.HDR).field("d").percentiles(50.0) + .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))) + .get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(percentiles("foo").method(PercentilesMethod.HDR).field("d").percentiles(50.0)).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MaxAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MaxAggregatorTests.java index 739419a534240..598d19795db20 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MaxAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MaxAggregatorTests.java @@ -110,6 +110,9 @@ public class MaxAggregatorTests extends AggregatorTestCase { /** Script to return the {@code _value} provided by aggs framework. */ public static final String VALUE_SCRIPT = "_value"; + /** Script to return a random double */ + public static final String RANDOM_SCRIPT = "Math.random()"; + @Override protected ScriptService getMockScriptService() { Map, Object>> scripts = new HashMap<>(); @@ -143,8 +146,12 @@ protected ScriptService getMockScriptService() { return ((Number) vars.get("_value")).doubleValue() + inc; }); + Map, Object>> nonDeterministicScripts = new HashMap<>(); + nonDeterministicScripts.put(RANDOM_SCRIPT, vars -> MaxAggregatorTests.randomDouble()); + MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, scripts, + nonDeterministicScripts, Collections.emptyMap()); Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); @@ -948,9 +955,10 @@ public void testCacheAggregation() throws IOException { } /** - * Make sure that an aggregation using a script does not get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws IOException { + public void testScriptCaching() throws Exception { Directory directory = newDirectory(); RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); final int numDocs = 10; @@ -968,7 +976,6 @@ public void testDontCacheScripts() throws IOException { MultiReader multiReader = new MultiReader(indexReader, unamappedIndexReader); IndexSearcher indexSearcher = newSearcher(multiReader, true, true); - MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); fieldType.setName("value"); MaxAggregationBuilder aggregationBuilder = new MaxAggregationBuilder("max") @@ -987,6 +994,24 @@ public void testDontCacheScripts() throws IOException { assertTrue(AggregationInspectionHelper.hasValue(max)); // Test that an aggregation using a script does not get cached + assertTrue(aggregator.context().getQueryShardContext().isCacheable()); + aggregationBuilder = new MaxAggregationBuilder("max") + .field("value") + .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, RANDOM_SCRIPT, Collections.emptyMap())); + + aggregator = createAggregator(aggregationBuilder, indexSearcher, fieldType); + aggregator.preCollection(); + indexSearcher.search(new MatchAllDocsQuery(), aggregator); + aggregator.postCollection(); + + max = (InternalMax) aggregator.buildAggregation(0L); + + assertTrue(max.getValue() >= 0.0); + assertTrue(max.getValue() <= 1.0); + assertEquals("max", max.getName()); + assertTrue(AggregationInspectionHelper.hasValue(max)); + + // Test that an aggregation using a nondeterministic script does not get cached assertFalse(aggregator.context().getQueryShardContext().isCacheable()); multiReader.close(); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MedianAbsoluteDeviationIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MedianAbsoluteDeviationIT.java index b40c8ee8acdf3..7345e16c2fc1c 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MedianAbsoluteDeviationIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MedianAbsoluteDeviationIT.java @@ -556,10 +556,10 @@ public void testOrderByEmptyAggregation() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked( prepareCreate("cache_test_idx") .addMapping("type", "d", "type=long") @@ -579,11 +579,11 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation(randomBuilder() .field("d") - .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))).get(); + .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() @@ -591,14 +591,25 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(randomBuilder().field("d")).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(randomBuilder() + .field("d") + .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(randomBuilder().field("d")).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MetricAggScriptPlugin.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MetricAggScriptPlugin.java index 362aef62ab23e..6787386862f9a 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MetricAggScriptPlugin.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MetricAggScriptPlugin.java @@ -28,6 +28,7 @@ import org.elasticsearch.script.MockScriptPlugin; import org.elasticsearch.search.lookup.LeafDocLookup; +import org.elasticsearch.test.ESTestCase; /** * Provides a number of dummy scripts for tests. @@ -52,6 +53,9 @@ public class MetricAggScriptPlugin extends MockScriptPlugin { /** Script to return the {@code _value} provided by aggs framework. */ public static final String VALUE_SCRIPT = "_value"; + /** Script to return a random double */ + public static final String RANDOM_SCRIPT = "Math.random()"; + @Override public String pluginScriptLang() { return METRIC_SCRIPT_ENGINE; @@ -88,4 +92,13 @@ protected Map, Object>> pluginScripts() { }); return scripts; } + + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("Math.random()", vars -> ESTestCase.randomDouble()); + + return scripts; + } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java index 922536a76544a..d6b65dfc62d74 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java @@ -127,6 +127,8 @@ public class MinAggregatorTests extends AggregatorTestCase { private static final String INVERT_SCRIPT = "invert"; + private static final String RANDOM_SCRIPT = "random"; + @Override protected ScriptService getMockScriptService() { Map, Object>> scripts = new HashMap<>(); @@ -161,8 +163,12 @@ protected ScriptService getMockScriptService() { }); scripts.put(INVERT_SCRIPT, vars -> -((Number) vars.get("_value")).doubleValue()); + Map, Object>> nonDeterministicScripts = new HashMap<>(); + nonDeterministicScripts.put(RANDOM_SCRIPT, vars -> AggregatorTestCase.randomDouble()); + MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, scripts, + nonDeterministicScripts, Collections.emptyMap()); Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); @@ -649,7 +655,7 @@ public void testCaching() throws IOException { } } - public void testNoCachingWithScript() throws IOException { + public void testScriptCaching() throws IOException { MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER); fieldType.setName("number"); @@ -657,6 +663,10 @@ public void testNoCachingWithScript() throws IOException { .field("number") .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, INVERT_SCRIPT, Collections.emptyMap()));; + MinAggregationBuilder nonDeterministicAggregationBuilder = new MinAggregationBuilder("min") + .field("number") + .script(new Script(ScriptType.INLINE, MockScriptEngine.NAME, RANDOM_SCRIPT, Collections.emptyMap()));; + try (Directory directory = newDirectory()) { RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); indexWriter.addDocument(singleton(new NumericDocValuesField("number", 7))); @@ -668,11 +678,19 @@ public void testNoCachingWithScript() throws IOException { try (IndexReader indexReader = DirectoryReader.open(directory)) { IndexSearcher indexSearcher = newSearcher(indexReader, true, true); - InternalMin min = searchAndReduce(indexSearcher, new MatchAllDocsQuery(), aggregationBuilder, fieldType); - assertEquals(-7.0, min.getValue(), 0); + InternalMin min = searchAndReduce(indexSearcher, new MatchAllDocsQuery(), nonDeterministicAggregationBuilder, fieldType); + assertTrue(min.getValue() >= 0.0 && min.getValue() <= 1.0); assertTrue(AggregationInspectionHelper.hasValue(min)); assertFalse(queryShardContext.isCacheable()); + + indexSearcher = newSearcher(indexReader, true, true); + + min = searchAndReduce(indexSearcher, new MatchAllDocsQuery(), aggregationBuilder, fieldType); + assertEquals(-7.0, min.getValue(), 0); + assertTrue(AggregationInspectionHelper.hasValue(min)); + + assertTrue(queryShardContext.isCacheable()); } } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java index 71ceeb1abeb72..406a96e3c34cd 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java @@ -247,6 +247,23 @@ protected Map, Object>> pluginScripts() { return scripts; } + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("state.data = Math.random()", vars -> + aggScript(vars, state -> state.put("data", ScriptedMetricIT.randomDouble()))); + + + scripts.put("state['count'] = Math.random() >= 0.5 ? 1 : 0", vars -> + aggScript(vars, state -> state.put("count", ScriptedMetricIT.randomDouble() >= 0.5 ? 1 : 0))); + + + scripts.put("return Math.random()", vars -> ScriptedMetricIT.randomDouble()); + + return scripts; + } + @SuppressWarnings("unchecked") static Map aggScript(Map vars, Consumer> fn) { Map aggState = (Map) vars.get("state"); @@ -1015,17 +1032,27 @@ public void testEmptyAggregation() throws Exception { assertThat(aggregationResult.get(0), equalTo(0)); } + /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script gets cached and nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { + // TODO(stu): add non-determinism in init, agg, combine and reduce, ensure not cached Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state['count'] = 1", Collections.emptyMap()); Script combineScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "no-op aggregation", Collections.emptyMap()); Script reduceScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "no-op list aggregation", Collections.emptyMap()); + Script ndInitScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.data = Math.random()", + Collections.emptyMap()); + + Script ndMapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state['count'] = Math.random() >= 0.5 ? 1 : 0", + Collections.emptyMap()); + + Script ndRandom = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "return Math.random()", + Collections.emptyMap()); + assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -1038,15 +1065,58 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a non-deterministic init script causes the result to not be cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0) - .addAggregation(scriptedMetric("foo").mapScript(mapScript).combineScript(combineScript).reduceScript(reduceScript)).get(); + .addAggregation(scriptedMetric("foo").initScript(ndInitScript).mapScript(mapScript).combineScript(combineScript) + .reduceScript(reduceScript)).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(0L)); + + // Test that a non-deterministic map script causes the result to not be cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(scriptedMetric("foo").mapScript(ndMapScript).combineScript(combineScript).reduceScript(reduceScript)) + .get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(0L)); + + // Test that a non-deterministic combine script causes the result to not be cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(scriptedMetric("foo").mapScript(mapScript).combineScript(ndRandom).reduceScript(reduceScript)).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(0L)); + + // NOTE: random reduce scripts don't hit the query shard context (they are done on the coordinator) and so can be cached. + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(scriptedMetric("foo").mapScript(mapScript).combineScript(combineScript).reduceScript(ndRandom)).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(1L)); + + // Test that all deterministic scripts cause the request to be cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(scriptedMetric("foo").mapScript(mapScript).combineScript(combineScript).reduceScript(reduceScript)) + .get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() - .getMissCount(), equalTo(0L)); + .getMissCount(), equalTo(2L)); } public void testConflictingAggAndScriptParams() { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/StatsIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/StatsIT.java index 13c5fd9e3e43b..bc3695edd9f0c 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/StatsIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/StatsIT.java @@ -488,10 +488,10 @@ private void assertShardExecutionState(SearchResponse response, int expectedFail } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -504,10 +504,10 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation( stats("foo").field("d").script( - new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value + 1", Collections.emptyMap()))).get(); + new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", Collections.emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() @@ -515,14 +515,24 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(stats("foo").field("d")).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation( + stats("foo").field("d").script( + new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value + 1", Collections.emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(stats("foo").field("d")).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/SumIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/SumIT.java index e9bef29d445be..87883852a615a 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/SumIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/SumIT.java @@ -47,6 +47,7 @@ import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; import static org.elasticsearch.search.aggregations.AggregationBuilders.terms; import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.METRIC_SCRIPT_ENGINE; +import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.RANDOM_SCRIPT; import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.SUM_VALUES_FIELD_SCRIPT; import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.VALUE_FIELD_SCRIPT; import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.VALUE_SCRIPT; @@ -374,10 +375,10 @@ public void testOrderByEmptyAggregation() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -390,10 +391,10 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation(sum("foo").field("d").script( - new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, VALUE_SCRIPT, Collections.emptyMap()))).get(); + new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, RANDOM_SCRIPT, Collections.emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() @@ -401,15 +402,25 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(sum("foo").field("d")).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(sum("foo").field("d").script( + new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, VALUE_SCRIPT, Collections.emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(sum("foo").field("d")).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } public void testFieldAlias() { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentileRanksIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentileRanksIT.java index 5051cee0dbf07..4974378d49010 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentileRanksIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentileRanksIT.java @@ -484,10 +484,10 @@ public void testOrderByEmptyAggregation() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -500,11 +500,11 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation(percentileRanks("foo", new double[]{50.0}) .field("d") - .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))).get(); + .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() @@ -512,15 +512,26 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentileRanks("foo", new double[]{50.0}).field("d")).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(percentileRanks("foo", new double[]{50.0}) + .field("d") + .script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentileRanks("foo", new double[]{50.0}).field("d")).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentilesIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentilesIT.java index e334213f1d2e1..7bd871e63fab4 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentilesIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TDigestPercentilesIT.java @@ -467,10 +467,10 @@ public void testOrderByEmptyAggregation() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -483,9 +483,9 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentiles("foo").field("d") - .percentiles(50.0).script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))) + .percentiles(50.0).script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "Math.random()", emptyMap()))) .get(); assertSearchResponse(r); @@ -494,14 +494,24 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentiles("foo").field("d").percentiles(50.0)).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentiles("foo").field("d") + .percentiles(50.0).script(new Script(ScriptType.INLINE, AggregationTestScriptsPlugin.NAME, "_value - 1", emptyMap()))) + .get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(percentiles("foo").field("d").percentiles(50.0)).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java index 417328bec4e63..207060d05ce72 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java @@ -107,6 +107,11 @@ public static class CustomScriptPlugin extends MockScriptPlugin { protected Map, Object>> pluginScripts() { return Collections.singletonMap("5", script -> "5"); } + + @Override + protected Map, Object>> nonDeterministicPluginScripts() { + return Collections.singletonMap("Math.random()", script -> TopHitsIT.randomDouble()); + } } public static String randomExecutionHint() { @@ -1086,10 +1091,10 @@ public void testNoStoredFields() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { try { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings( @@ -1107,10 +1112,10 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script field does not get cached + // Test that a request using a nondeterministic script field does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation(topHits("foo").scriptField("bar", - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "5", Collections.emptyMap()))).get(); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() @@ -1118,11 +1123,12 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script sort does not get cached + // Test that a request using a nondeterministic script sort does not get cached r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation(topHits("foo").sort( SortBuilders.scriptSort( - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "5", Collections.emptyMap()), ScriptSortType.STRING))) + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "Math.random()", Collections.emptyMap()), + ScriptSortType.STRING))) .get(); assertSearchResponse(r); @@ -1131,15 +1137,38 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(topHits("foo")).get(); + // Test that a request using a deterministic script field does not get cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(topHits("foo").scriptField("bar", + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "5", Collections.emptyMap()))).get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Test that a request using a deterministic script sort does not get cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(topHits("foo").sort( + SortBuilders.scriptSort( + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "5", Collections.emptyMap()), ScriptSortType.STRING))) + .get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(topHits("foo")).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(3L)); } finally { assertAcked(client().admin().indices().prepareDelete("cache_test_idx")); // delete this - if we use tests.iters it would fail } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ValueCountIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ValueCountIT.java index 2976d1310bcf9..642326010a2f8 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ValueCountIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ValueCountIT.java @@ -43,6 +43,7 @@ import static org.elasticsearch.search.aggregations.AggregationBuilders.global; import static org.elasticsearch.search.aggregations.AggregationBuilders.terms; import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.METRIC_SCRIPT_ENGINE; +import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.RANDOM_SCRIPT; import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.SUM_FIELD_PARAMS_SCRIPT; import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.SUM_VALUES_FIELD_SCRIPT; import static org.elasticsearch.search.aggregations.metrics.MetricAggScriptPlugin.VALUE_FIELD_SCRIPT; @@ -211,10 +212,10 @@ public void testMultiValuedScriptWithParams() throws Exception { } /** - * Make sure that a request using a script does not get cached and a request - * not using a script does get cached. + * Make sure that a request using a deterministic script or not using a script get cached. + * Ensure requests using nondeterministic scripts do not get cached. */ - public void testDontCacheScripts() throws Exception { + public void testScriptCaching() throws Exception { assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -227,10 +228,10 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // Test that a request using a script does not get cached + // Test that a request using a nondeterministic script does not get cached SearchResponse r = client().prepareSearch("cache_test_idx").setSize(0) .addAggregation(count("foo").field("d").script( - new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, VALUE_FIELD_SCRIPT, Collections.emptyMap()))) + new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, RANDOM_SCRIPT, Collections.emptyMap()))) .get(); assertSearchResponse(r); @@ -239,15 +240,26 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(0L)); - // To make sure that the cache is working test that a request not using - // a script is cached - r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(count("foo").field("d")).get(); + // Test that a request using a deterministic script gets cached + r = client().prepareSearch("cache_test_idx").setSize(0) + .addAggregation(count("foo").field("d").script( + new Script(ScriptType.INLINE, METRIC_SCRIPT_ENGINE, VALUE_FIELD_SCRIPT, Collections.emptyMap()))) + .get(); assertSearchResponse(r); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getHitCount(), equalTo(0L)); assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); + + // Ensure that non-scripted requests are cached as normal + r = client().prepareSearch("cache_test_idx").setSize(0).addAggregation(count("foo").field("d")).get(); + assertSearchResponse(r); + + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getHitCount(), equalTo(0L)); + assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() + .getMissCount(), equalTo(2L)); } public void testOrderByEmptyAggregation() throws Exception { diff --git a/test/framework/src/main/java/org/elasticsearch/script/MockDeterministicScript.java b/test/framework/src/main/java/org/elasticsearch/script/MockDeterministicScript.java new file mode 100644 index 0000000000000..8cafd512450a7 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/script/MockDeterministicScript.java @@ -0,0 +1,45 @@ +/* + * 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. + */ + +package org.elasticsearch.script; + +import java.util.Map; +import java.util.function.Function; + +/** + * A mocked script used for testing purposes. {@code deterministic} implies cacheability in query shard cache. + */ +public abstract class MockDeterministicScript implements Function, Object>, ScriptFactory { + public abstract Object apply(Map vars); + public abstract boolean isResultDeterministic(); + + public static MockDeterministicScript asDeterministic(Function, Object> script) { + return new MockDeterministicScript() { + @Override public boolean isResultDeterministic() { return true; } + @Override public Object apply(Map vars) { return script.apply(vars); } + }; + } + + public static MockDeterministicScript asNonDeterministic(Function, Object> script) { + return new MockDeterministicScript() { + @Override public boolean isResultDeterministic() { return false; } + @Override public Object apply(Map vars) { return script.apply(vars); } + }; + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java index 84aad77377a30..3069efbebb451 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java @@ -62,11 +62,22 @@ public interface ContextCompiler { public static final String NAME = "mockscript"; private final String type; - private final Map, Object>> scripts; + private final Map scripts; private final Map, ContextCompiler> contexts; public MockScriptEngine(String type, Map, Object>> scripts, Map, ContextCompiler> contexts) { + this(type, scripts, Collections.emptyMap(), contexts); + } + + public MockScriptEngine(String type, Map, Object>> deterministicScripts, + Map, Object>> nonDeterministicScripts, + Map, ContextCompiler> contexts) { + + Map scripts = new HashMap<>(deterministicScripts.size() + nonDeterministicScripts.size()); + deterministicScripts.forEach((key, value) -> scripts.put(key, MockDeterministicScript.asDeterministic(value))); + nonDeterministicScripts.forEach((key, value) -> scripts.put(key, MockDeterministicScript.asNonDeterministic(value))); + this.type = type; this.scripts = Collections.unmodifiableMap(scripts); this.contexts = Collections.unmodifiableMap(contexts); @@ -85,34 +96,14 @@ public String getType() { public T compile(String name, String source, ScriptContext context, Map params) { // Scripts are always resolved using the script's source. For inline scripts, it's easy because they don't have names and the // source is always provided. For stored and file scripts, the source of the script must match the key of a predefined script. - Function, Object> script = scripts.get(source); + MockDeterministicScript script = scripts.get(source); if (script == null) { throw new IllegalArgumentException("No pre defined script matching [" + source + "] for script with name [" + name + "], " + "did you declare the mocked script?"); } MockCompiledScript mockCompiled = new MockCompiledScript(name, params, source, script); if (context.instanceClazz.equals(FieldScript.class)) { - FieldScript.Factory factory = (parameters, lookup) -> - ctx -> new FieldScript(parameters, lookup, ctx) { - @Override - public Object execute() { - Map vars = createVars(parameters); - vars.putAll(getLeafLookup().asMap()); - return script.apply(vars); - } - }; - return context.factoryClazz.cast(factory); - } else if (context.instanceClazz.equals(FieldScript.class)) { - FieldScript.Factory factory = (parameters, lookup) -> - ctx -> new FieldScript(parameters, lookup, ctx) { - @Override - public Object execute() { - Map vars = createVars(parameters); - vars.putAll(getLeafLookup().asMap()); - return script.apply(vars); - } - }; - return context.factoryClazz.cast(factory); + return context.factoryClazz.cast(new MockFieldScriptFactory(script)); } else if(context.instanceClazz.equals(TermsSetQueryScript.class)) { TermsSetQueryScript.Factory factory = (parameters, lookup) -> (TermsSetQueryScript.LeafFactory) ctx -> new TermsSetQueryScript(parameters, lookup, ctx) { @@ -147,17 +138,7 @@ public boolean needs_score() { }; return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(StringSortScript.class)) { - StringSortScript.Factory factory = (parameters, lookup) -> (StringSortScript.LeafFactory) ctx - -> new StringSortScript(parameters, lookup, ctx) { - @Override - public String execute() { - Map vars = new HashMap<>(parameters); - vars.put("params", parameters); - vars.put("doc", getDoc()); - return String.valueOf(script.apply(vars)); - } - }; - return context.factoryClazz.cast(factory); + return context.factoryClazz.cast(new MockStringSortScriptFactory(script)); } else if (context.instanceClazz.equals(IngestScript.class)) { IngestScript.Factory factory = vars -> new IngestScript(vars) { @Override @@ -167,37 +148,7 @@ public void execute(Map ctx) { }; return context.factoryClazz.cast(factory); } else if(context.instanceClazz.equals(AggregationScript.class)) { - AggregationScript.Factory factory = (parameters, lookup) -> new AggregationScript.LeafFactory() { - @Override - public AggregationScript newInstance(final LeafReaderContext ctx) { - return new AggregationScript(parameters, lookup, ctx) { - @Override - public Object execute() { - Map vars = new HashMap<>(parameters); - vars.put("params", parameters); - vars.put("doc", getDoc()); - vars.put("_score", get_score()); - vars.put("_value", get_value()); - return script.apply(vars); - } - }; - } - - @Override - public boolean needs_score() { - return true; - } - }; - return context.factoryClazz.cast(factory); - } else if (context.instanceClazz.equals(IngestScript.class)) { - IngestScript.Factory factory = vars -> - new IngestScript(vars) { - @Override - public void execute(Map ctx) { - script.apply(ctx); - } - }; - return context.factoryClazz.cast(factory); + return context.factoryClazz.cast(new MockAggregationScript(script)); } else if (context.instanceClazz.equals(IngestConditionalScript.class)) { IngestConditionalScript.Factory factory = parameters -> new IngestConditionalScript(parameters) { @Override @@ -240,13 +191,7 @@ public boolean execute() { }; return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(SignificantTermsHeuristicScoreScript.class)) { - SignificantTermsHeuristicScoreScript.Factory factory = () -> new SignificantTermsHeuristicScoreScript() { - @Override - public double execute(Map vars) { - return ((Number) script.apply(vars)).doubleValue(); - } - }; - return context.factoryClazz.cast(factory); + return context.factoryClazz.cast(new MockSignificantTermsHeuristicScoreScript(script)); } else if (context.instanceClazz.equals(TemplateScript.class)) { TemplateScript.Factory factory = vars -> { Map varsWithParams = new HashMap<>(); @@ -280,19 +225,19 @@ public double execute(Map params1, double[] values) { }; return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(ScoreScript.class)) { - ScoreScript.Factory factory = new MockScoreScript(script); + ScoreScript.Factory factory = new MockScoreScript(script::apply); return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(ScriptedMetricAggContexts.InitScript.class)) { - ScriptedMetricAggContexts.InitScript.Factory factory = mockCompiled::createMetricAggInitScript; + ScriptedMetricAggContexts.InitScript.Factory factory = new MockMetricAggInitScriptFactory(script); return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(ScriptedMetricAggContexts.MapScript.class)) { - ScriptedMetricAggContexts.MapScript.Factory factory = mockCompiled::createMetricAggMapScript; + ScriptedMetricAggContexts.MapScript.Factory factory = new MockMetricAggMapScriptFactory(script); return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(ScriptedMetricAggContexts.CombineScript.class)) { - ScriptedMetricAggContexts.CombineScript.Factory factory = mockCompiled::createMetricAggCombineScript; + ScriptedMetricAggContexts.CombineScript.Factory factory = new MockMetricAggCombineScriptFactory(script); return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(ScriptedMetricAggContexts.ReduceScript.class)) { - ScriptedMetricAggContexts.ReduceScript.Factory factory = mockCompiled::createMetricAggReduceScript; + ScriptedMetricAggContexts.ReduceScript.Factory factory = new MockMetricAggReduceScriptFactory(script); return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(IntervalFilterScript.class)) { IntervalFilterScript.Factory factory = mockCompiled::createIntervalFilterScript; @@ -300,7 +245,7 @@ public double execute(Map params1, double[] values) { } ContextCompiler compiler = contexts.get(context); if (compiler != null) { - return context.factoryClazz.cast(compiler.compile(script, params)); + return context.factoryClazz.cast(compiler.compile(script::apply, params)); } throw new IllegalArgumentException("mock script engine does not know how to handle context [" + context.name + "]"); } @@ -370,25 +315,6 @@ public SimilarityWeightScript createSimilarityWeightScript() { return new MockSimilarityWeightScript(script != null ? script : ctx -> 42d); } - public ScriptedMetricAggContexts.InitScript createMetricAggInitScript(Map params, Map state) { - return new MockMetricAggInitScript(params, state, script != null ? script : ctx -> 42d); - } - - public ScriptedMetricAggContexts.MapScript.LeafFactory createMetricAggMapScript(Map params, - Map state, - SearchLookup lookup) { - return new MockMetricAggMapScript(params, state, lookup, script != null ? script : ctx -> 42d); - } - - public ScriptedMetricAggContexts.CombineScript createMetricAggCombineScript(Map params, - Map state) { - return new MockMetricAggCombineScript(params, state, script != null ? script : ctx -> 42d); - } - - public ScriptedMetricAggContexts.ReduceScript createMetricAggReduceScript(Map params, List states) { - return new MockMetricAggReduceScript(params, states, script != null ? script : ctx -> 42d); - } - public IntervalFilterScript createIntervalFilterScript() { return new IntervalFilterScript() { @Override @@ -469,6 +395,17 @@ public double execute(Query query, Field field, Term term) { } } + public static class MockMetricAggInitScriptFactory implements ScriptedMetricAggContexts.InitScript.Factory, ScriptFactory { + private final MockDeterministicScript script; + MockMetricAggInitScriptFactory(MockDeterministicScript script) { this.script = script; } + @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } + + @Override + public ScriptedMetricAggContexts.InitScript newInstance(Map params, Map state) { + return new MockMetricAggInitScript(params, state, script); + } + } + public static class MockMetricAggInitScript extends ScriptedMetricAggContexts.InitScript { private final Function, Object> script; @@ -491,6 +428,18 @@ public void execute() { } } + public static class MockMetricAggMapScriptFactory implements ScriptedMetricAggContexts.MapScript.Factory, ScriptFactory { + private final MockDeterministicScript script; + MockMetricAggMapScriptFactory(MockDeterministicScript script) { this.script = script; } + @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } + + @Override + public ScriptedMetricAggContexts.MapScript.LeafFactory newFactory(Map params, Map state, + SearchLookup lookup) { + return new MockMetricAggMapScript(params, state, lookup, script); + } + } + public static class MockMetricAggMapScript implements ScriptedMetricAggContexts.MapScript.LeafFactory { private final Map params; private final Map state; @@ -527,11 +476,21 @@ public void execute() { } } + public static class MockMetricAggCombineScriptFactory implements ScriptedMetricAggContexts.CombineScript.Factory, ScriptFactory { + private final MockDeterministicScript script; + MockMetricAggCombineScriptFactory(MockDeterministicScript script) { this.script = script; } + @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } + + @Override + public ScriptedMetricAggContexts.CombineScript newInstance(Map params, Map state) { + return new MockMetricAggCombineScript(params, state, script); + } + } + public static class MockMetricAggCombineScript extends ScriptedMetricAggContexts.CombineScript { private final Function, Object> script; - MockMetricAggCombineScript(Map params, Map state, - Function, Object> script) { + MockMetricAggCombineScript(Map params, Map state, Function, Object> script) { super(params, state); this.script = script; } @@ -549,11 +508,21 @@ public Object execute() { } } + public static class MockMetricAggReduceScriptFactory implements ScriptedMetricAggContexts.ReduceScript.Factory, ScriptFactory { + private final MockDeterministicScript script; + MockMetricAggReduceScriptFactory(MockDeterministicScript script) { this.script = script; } + @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } + + @Override + public ScriptedMetricAggContexts.ReduceScript newInstance(Map params, List states) { + return new MockMetricAggReduceScript(params, states, script); + } + } + public static class MockMetricAggReduceScript extends ScriptedMetricAggContexts.ReduceScript { private final Function, Object> script; - MockMetricAggReduceScript(Map params, List states, - Function, Object> script) { + MockMetricAggReduceScript(Map params, List states, Function, Object> script) { super(params, states); this.script = script; } @@ -615,4 +584,88 @@ public void setScorer(Scorable scorer) { } } + class MockAggregationScript implements AggregationScript.Factory, ScriptFactory { + private final MockDeterministicScript script; + MockAggregationScript(MockDeterministicScript script) { this.script = script; } + @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } + + @Override + public AggregationScript.LeafFactory newFactory(Map params, SearchLookup lookup) { + return new AggregationScript.LeafFactory() { + @Override + public AggregationScript newInstance(final LeafReaderContext ctx) { + return new AggregationScript(params, lookup, ctx) { + @Override + public Object execute() { + Map vars = new HashMap<>(params); + vars.put("params", params); + vars.put("doc", getDoc()); + vars.put("_score", get_score()); + vars.put("_value", get_value()); + return script.apply(vars); + } + }; + } + + @Override + public boolean needs_score() { + return true; + } + }; + } + } + + class MockSignificantTermsHeuristicScoreScript implements SignificantTermsHeuristicScoreScript.Factory, ScriptFactory { + private final MockDeterministicScript script; + MockSignificantTermsHeuristicScoreScript(MockDeterministicScript script) { this.script = script; } + @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } + + @Override + public SignificantTermsHeuristicScoreScript newInstance() { + return new SignificantTermsHeuristicScoreScript() { + @Override + public double execute(Map vars) { + return ((Number) script.apply(vars)).doubleValue(); + } + }; + } + } + + class MockFieldScriptFactory implements FieldScript.Factory, ScriptFactory { + private final MockDeterministicScript script; + MockFieldScriptFactory(MockDeterministicScript script) { this.script = script; } + @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } + + @Override + public FieldScript.LeafFactory newFactory(Map parameters, SearchLookup lookup) { + return ctx -> new FieldScript(parameters, lookup, ctx) { + @Override + public Object execute() { + Map vars = createVars(parameters); + vars.putAll(getLeafLookup().asMap()); + return script.apply(vars); + + } + }; + } + } + + class MockStringSortScriptFactory implements StringSortScript.Factory, ScriptFactory { + private final MockDeterministicScript script; + MockStringSortScriptFactory(MockDeterministicScript script) { this.script = script; } + @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } + + @Override + public StringSortScript.LeafFactory newFactory(Map parameters, SearchLookup lookup) { + return ctx -> new StringSortScript(parameters, lookup, ctx) { + @Override + public String execute() { + Map vars = new HashMap<>(parameters); + vars.put("params", parameters); + vars.put("doc", getDoc()); + return String.valueOf(script.apply(vars)); + } + }; + } + } } diff --git a/test/framework/src/main/java/org/elasticsearch/script/MockScriptPlugin.java b/test/framework/src/main/java/org/elasticsearch/script/MockScriptPlugin.java index 34aca79ec4725..972879a735a72 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptPlugin.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptPlugin.java @@ -37,11 +37,13 @@ public abstract class MockScriptPlugin extends Plugin implements ScriptPlugin { @Override public ScriptEngine getScriptEngine(Settings settings, Collection> contexts) { - return new MockScriptEngine(pluginScriptLang(), pluginScripts(), pluginContextCompilers()); + return new MockScriptEngine(pluginScriptLang(), pluginScripts(), nonDeterministicPluginScripts(), pluginContextCompilers()); } protected abstract Map, Object>> pluginScripts(); + protected Map, Object>> nonDeterministicPluginScripts() { return Collections.emptyMap(); } + protected Map, MockScriptEngine.ContextCompiler> pluginContextCompilers() { return Collections.emptyMap(); } From f6380f1d4ebee1b79199ca649b7a729433d54bc7 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Wed, 18 Dec 2019 07:28:56 -1000 Subject: [PATCH 255/686] Geo: Switch generated WKT to upper case (#50285) Switches generated WKT to upper case to conform to the standard recommendation. Relates #49568 --- .../ingest/processors/circle.asciidoc | 2 +- .../geometry/utils/WellKnownText.java | 18 ++-- .../elasticsearch/geometry/CircleTests.java | 8 +- .../geometry/GeometryCollectionTests.java | 8 +- .../org/elasticsearch/geometry/LineTests.java | 12 +-- .../geometry/MultiLineTests.java | 8 +- .../geometry/MultiPointTests.java | 16 +-- .../geometry/MultiPolygonTests.java | 8 +- .../elasticsearch/geometry/PointTests.java | 10 +- .../elasticsearch/geometry/PolygonTests.java | 18 ++-- .../geometry/RectangleTests.java | 8 +- .../common/geo/GeometryParserTests.java | 2 +- .../qa/src/main/resources/docs/geo.csv-spec | 4 +- .../qa/src/main/resources/geo/geosql.csv-spec | 98 +++++++++---------- .../scalar/geo/GeoProcessorTests.java | 4 +- .../scalar/geo/StWkttosqlProcessorTests.java | 4 +- .../sql/planner/QueryTranslatorTests.java | 8 +- 17 files changed, 118 insertions(+), 118 deletions(-) diff --git a/docs/reference/ingest/processors/circle.asciidoc b/docs/reference/ingest/processors/circle.asciidoc index e2d5c428b9d5d..300d7763fe634 100644 --- a/docs/reference/ingest/processors/circle.asciidoc +++ b/docs/reference/ingest/processors/circle.asciidoc @@ -80,7 +80,7 @@ The response from the above index request: "_seq_no": 22, "_primary_term": 1, "_source": { - "circle": "polygon ((30.000365257263184 10.0, 30.000111397193788 10.00034284530941, 29.999706043744222 10.000213571721195, 29.999706043744222 9.999786428278805, 30.000111397193788 9.99965715469059, 30.000365257263184 10.0))" + "circle": "POLYGON ((30.000365257263184 10.0, 30.000111397193788 10.00034284530941, 29.999706043744222 10.000213571721195, 29.999706043744222 9.999786428278805, 30.000111397193788 9.99965715469059, 30.000365257263184 10.0))" } } -------------------------------------------------- diff --git a/libs/geo/src/main/java/org/elasticsearch/geometry/utils/WellKnownText.java b/libs/geo/src/main/java/org/elasticsearch/geometry/utils/WellKnownText.java index a59820785e968..a71ce222f1e9e 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geometry/utils/WellKnownText.java +++ b/libs/geo/src/main/java/org/elasticsearch/geometry/utils/WellKnownText.java @@ -586,17 +586,17 @@ private static String getWKTName(Geometry geometry) { return geometry.visit(new GeometryVisitor() { @Override public String visit(Circle circle) { - return "circle"; + return "CIRCLE"; } @Override public String visit(GeometryCollection collection) { - return "geometrycollection"; + return "GEOMETRYCOLLECTION"; } @Override public String visit(Line line) { - return "linestring"; + return "LINESTRING"; } @Override @@ -606,32 +606,32 @@ public String visit(LinearRing ring) { @Override public String visit(MultiLine multiLine) { - return "multilinestring"; + return "MULTILINESTRING"; } @Override public String visit(MultiPoint multiPoint) { - return "multipoint"; + return "MULTIPOINT"; } @Override public String visit(MultiPolygon multiPolygon) { - return "multipolygon"; + return "MULTIPOLYGON"; } @Override public String visit(Point point) { - return "point"; + return "POINT"; } @Override public String visit(Polygon polygon) { - return "polygon"; + return "POLYGON"; } @Override public String visit(Rectangle rectangle) { - return "bbox"; + return "BBOX"; } }); } diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/CircleTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/CircleTests.java index eba391dc97038..9f42118821fe7 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/CircleTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/CircleTests.java @@ -40,14 +40,14 @@ protected Circle createTestInstance(boolean hasAlt) { public void testBasicSerialization() throws IOException, ParseException { WellKnownText wkt = new WellKnownText(true, new GeographyValidator(true)); - assertEquals("circle (20.0 10.0 15.0)", wkt.toWKT(new Circle(20, 10, 15))); + assertEquals("CIRCLE (20.0 10.0 15.0)", wkt.toWKT(new Circle(20, 10, 15))); assertEquals(new Circle(20, 10, 15), wkt.fromWKT("circle (20.0 10.0 15.0)")); - assertEquals("circle (20.0 10.0 15.0 25.0)", wkt.toWKT(new Circle(20, 10, 25, 15))); + assertEquals("CIRCLE (20.0 10.0 15.0 25.0)", wkt.toWKT(new Circle(20, 10, 25, 15))); assertEquals(new Circle(20, 10, 25, 15), wkt.fromWKT("circle (20.0 10.0 15.0 25.0)")); - assertEquals("circle EMPTY", wkt.toWKT(Circle.EMPTY)); - assertEquals(Circle.EMPTY, wkt.fromWKT("circle EMPTY)")); + assertEquals("CIRCLE EMPTY", wkt.toWKT(Circle.EMPTY)); + assertEquals(Circle.EMPTY, wkt.fromWKT("CIRCLE EMPTY)")); } public void testInitValidation() { diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/GeometryCollectionTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/GeometryCollectionTests.java index 0783983131ea0..a0898242546df 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/GeometryCollectionTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/GeometryCollectionTests.java @@ -37,14 +37,14 @@ protected GeometryCollection createTestInstance(boolean hasAlt) { public void testBasicSerialization() throws IOException, ParseException { WellKnownText wkt = new WellKnownText(true, new GeographyValidator(true)); - assertEquals("geometrycollection (point (20.0 10.0),point EMPTY)", + assertEquals("GEOMETRYCOLLECTION (POINT (20.0 10.0),POINT EMPTY)", wkt.toWKT(new GeometryCollection(Arrays.asList(new Point(20, 10), Point.EMPTY)))); assertEquals(new GeometryCollection(Arrays.asList(new Point(20, 10), Point.EMPTY)), - wkt.fromWKT("geometrycollection (point (20.0 10.0),point EMPTY)")); + wkt.fromWKT("GEOMETRYCOLLECTION (POINT (20.0 10.0),POINT EMPTY)")); - assertEquals("geometrycollection EMPTY", wkt.toWKT(GeometryCollection.EMPTY)); - assertEquals(GeometryCollection.EMPTY, wkt.fromWKT("geometrycollection EMPTY)")); + assertEquals("GEOMETRYCOLLECTION EMPTY", wkt.toWKT(GeometryCollection.EMPTY)); + assertEquals(GeometryCollection.EMPTY, wkt.fromWKT("GEOMETRYCOLLECTION EMPTY)")); } @SuppressWarnings("ConstantConditions") diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/LineTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/LineTests.java index 7342e0b562d19..0ca38c185ef46 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/LineTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/LineTests.java @@ -36,16 +36,16 @@ protected Line createTestInstance(boolean hasAlt) { public void testBasicSerialization() throws IOException, ParseException { WellKnownText wkt = new WellKnownText(true, new GeographyValidator(true)); - assertEquals("linestring (3.0 1.0, 4.0 2.0)", wkt.toWKT(new Line(new double[]{3, 4}, new double[]{1, 2}))); - assertEquals(new Line(new double[]{3, 4}, new double[]{1, 2}), wkt.fromWKT("linestring (3 1, 4 2)")); + assertEquals("LINESTRING (3.0 1.0, 4.0 2.0)", wkt.toWKT(new Line(new double[]{3, 4}, new double[]{1, 2}))); + assertEquals(new Line(new double[]{3, 4}, new double[]{1, 2}), wkt.fromWKT("LINESTRING (3 1, 4 2)")); - assertEquals("linestring (3.0 1.0 5.0, 4.0 2.0 6.0)", wkt.toWKT(new Line(new double[]{3, 4}, new double[]{1, 2}, + assertEquals("LINESTRING (3.0 1.0 5.0, 4.0 2.0 6.0)", wkt.toWKT(new Line(new double[]{3, 4}, new double[]{1, 2}, new double[]{5, 6}))); assertEquals(new Line(new double[]{3, 4}, new double[]{1, 2}, new double[]{6, 5}), - wkt.fromWKT("linestring (3 1 6, 4 2 5)")); + wkt.fromWKT("LINESTRING (3 1 6, 4 2 5)")); - assertEquals("linestring EMPTY", wkt.toWKT(Line.EMPTY)); - assertEquals(Line.EMPTY, wkt.fromWKT("linestring EMPTY)")); + assertEquals("LINESTRING EMPTY", wkt.toWKT(Line.EMPTY)); + assertEquals(Line.EMPTY, wkt.fromWKT("LINESTRING EMPTY)")); } public void testInitValidation() { diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/MultiLineTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/MultiLineTests.java index 4610932e0afbf..260eb5facf462 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/MultiLineTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/MultiLineTests.java @@ -44,13 +44,13 @@ protected MultiLine createTestInstance(boolean hasAlt) { public void testBasicSerialization() throws IOException, ParseException { WellKnownText wkt = new WellKnownText(true, new GeographyValidator(true)); - assertEquals("multilinestring ((3.0 1.0, 4.0 2.0))", wkt.toWKT( + assertEquals("MULTILINESTRING ((3.0 1.0, 4.0 2.0))", wkt.toWKT( new MultiLine(Collections.singletonList(new Line(new double[]{3, 4}, new double[]{1, 2}))))); assertEquals(new MultiLine(Collections.singletonList(new Line(new double[]{3, 4}, new double[]{1, 2}))), - wkt.fromWKT("multilinestring ((3 1, 4 2))")); + wkt.fromWKT("MULTILINESTRING ((3 1, 4 2))")); - assertEquals("multilinestring EMPTY", wkt.toWKT(MultiLine.EMPTY)); - assertEquals(MultiLine.EMPTY, wkt.fromWKT("multilinestring EMPTY)")); + assertEquals("MULTILINESTRING EMPTY", wkt.toWKT(MultiLine.EMPTY)); + assertEquals(MultiLine.EMPTY, wkt.fromWKT("MULTILINESTRING EMPTY)")); } public void testValidation() { diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/MultiPointTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/MultiPointTests.java index 469801279ee17..c512603d0b762 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/MultiPointTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/MultiPointTests.java @@ -45,23 +45,23 @@ protected MultiPoint createTestInstance(boolean hasAlt) { public void testBasicSerialization() throws IOException, ParseException { WellKnownText wkt = new WellKnownText(true, new GeographyValidator(true)); - assertEquals("multipoint (2.0 1.0)", wkt.toWKT( + assertEquals("MULTIPOINT (2.0 1.0)", wkt.toWKT( new MultiPoint(Collections.singletonList(new Point(2, 1))))); assertEquals(new MultiPoint(Collections.singletonList(new Point(2, 1))), - wkt.fromWKT("multipoint (2 1)")); + wkt.fromWKT("MULTIPOINT (2 1)")); - assertEquals("multipoint (2.0 1.0, 3.0 4.0)", + assertEquals("MULTIPOINT (2.0 1.0, 3.0 4.0)", wkt.toWKT(new MultiPoint(Arrays.asList(new Point(2, 1), new Point(3, 4))))); assertEquals(new MultiPoint(Arrays.asList(new Point(2, 1), new Point(3, 4))), - wkt.fromWKT("multipoint (2 1, 3 4)")); + wkt.fromWKT("MULTIPOINT (2 1, 3 4)")); - assertEquals("multipoint (2.0 1.0 10.0, 3.0 4.0 20.0)", + assertEquals("MULTIPOINT (2.0 1.0 10.0, 3.0 4.0 20.0)", wkt.toWKT(new MultiPoint(Arrays.asList(new Point(2, 1, 10), new Point(3, 4, 20))))); assertEquals(new MultiPoint(Arrays.asList(new Point(2, 1, 10), new Point(3, 4, 20))), - wkt.fromWKT("multipoint (2 1 10, 3 4 20)")); + wkt.fromWKT("MULTIPOINT (2 1 10, 3 4 20)")); - assertEquals("multipoint EMPTY", wkt.toWKT(MultiPoint.EMPTY)); - assertEquals(MultiPoint.EMPTY, wkt.fromWKT("multipoint EMPTY)")); + assertEquals("MULTIPOINT EMPTY", wkt.toWKT(MultiPoint.EMPTY)); + assertEquals(MultiPoint.EMPTY, wkt.fromWKT("MULTIPOINT EMPTY)")); } public void testValidation() { diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/MultiPolygonTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/MultiPolygonTests.java index dfd8142ae2c9e..ee1d9a6a5873c 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/MultiPolygonTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/MultiPolygonTests.java @@ -44,15 +44,15 @@ protected MultiPolygon createTestInstance(boolean hasAlt) { public void testBasicSerialization() throws IOException, ParseException { WellKnownText wkt = new WellKnownText(true, new GeographyValidator(true)); - assertEquals("multipolygon (((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0)))", + assertEquals("MULTIPOLYGON (((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0)))", wkt.toWKT(new MultiPolygon(Collections.singletonList( new Polygon(new LinearRing(new double[]{3, 4, 5, 3}, new double[]{1, 2, 3, 1})))))); assertEquals(new MultiPolygon(Collections.singletonList( new Polygon(new LinearRing(new double[]{3, 4, 5, 3}, new double[]{1, 2, 3, 1})))), - wkt.fromWKT("multipolygon (((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0)))")); + wkt.fromWKT("MULTIPOLYGON (((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0)))")); - assertEquals("multipolygon EMPTY", wkt.toWKT(MultiPolygon.EMPTY)); - assertEquals(MultiPolygon.EMPTY, wkt.fromWKT("multipolygon EMPTY)")); + assertEquals("MULTIPOLYGON EMPTY", wkt.toWKT(MultiPolygon.EMPTY)); + assertEquals(MultiPolygon.EMPTY, wkt.fromWKT("MULTIPOLYGON EMPTY)")); } public void testValidation() { diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/PointTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/PointTests.java index 70920aac49b88..79c9371e58ce6 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/PointTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/PointTests.java @@ -36,14 +36,14 @@ protected Point createTestInstance(boolean hasAlt) { public void testBasicSerialization() throws IOException, ParseException { WellKnownText wkt = new WellKnownText(true, new GeographyValidator(true)); - assertEquals("point (20.0 10.0)", wkt.toWKT(new Point(20, 10))); + assertEquals("POINT (20.0 10.0)", wkt.toWKT(new Point(20, 10))); assertEquals(new Point(20, 10), wkt.fromWKT("point (20.0 10.0)")); - assertEquals("point (20.0 10.0 100.0)", wkt.toWKT(new Point(20, 10, 100))); - assertEquals(new Point(20, 10, 100), wkt.fromWKT("point (20.0 10.0 100.0)")); + assertEquals("POINT (20.0 10.0 100.0)", wkt.toWKT(new Point(20, 10, 100))); + assertEquals(new Point(20, 10, 100), wkt.fromWKT("POINT (20.0 10.0 100.0)")); - assertEquals("point EMPTY", wkt.toWKT(Point.EMPTY)); - assertEquals(Point.EMPTY, wkt.fromWKT("point EMPTY)")); + assertEquals("POINT EMPTY", wkt.toWKT(Point.EMPTY)); + assertEquals(Point.EMPTY, wkt.fromWKT("POINT EMPTY)")); } public void testInitValidation() { diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/PolygonTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/PolygonTests.java index dd2c0552101d6..c90243d253df5 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/PolygonTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/PolygonTests.java @@ -36,27 +36,27 @@ protected Polygon createTestInstance(boolean hasAlt) { public void testBasicSerialization() throws IOException, ParseException { WellKnownText wkt = new WellKnownText(true, new GeographyValidator(true)); - assertEquals("polygon ((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0))", + assertEquals("POLYGON ((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0))", wkt.toWKT(new Polygon(new LinearRing(new double[]{3, 4, 5, 3}, new double[]{1, 2, 3, 1})))); assertEquals(new Polygon(new LinearRing(new double[]{3, 4, 5, 3}, new double[]{1, 2, 3, 1})), - wkt.fromWKT("polygon ((3 1, 4 2, 5 3, 3 1))")); + wkt.fromWKT("POLYGON ((3 1, 4 2, 5 3, 3 1))")); - assertEquals("polygon ((3.0 1.0 5.0, 4.0 2.0 4.0, 5.0 3.0 3.0, 3.0 1.0 5.0))", + assertEquals("POLYGON ((3.0 1.0 5.0, 4.0 2.0 4.0, 5.0 3.0 3.0, 3.0 1.0 5.0))", wkt.toWKT(new Polygon(new LinearRing(new double[]{3, 4, 5, 3}, new double[]{1, 2, 3, 1}, new double[]{5, 4, 3, 5})))); assertEquals(new Polygon(new LinearRing(new double[]{3, 4, 5, 3}, new double[]{1, 2, 3, 1}, new double[]{5, 4, 3, 5})), - wkt.fromWKT("polygon ((3 1 5, 4 2 4, 5 3 3, 3 1 5))")); + wkt.fromWKT("POLYGON ((3 1 5, 4 2 4, 5 3 3, 3 1 5))")); // Auto closing in coerce mode assertEquals(new Polygon(new LinearRing(new double[]{3, 4, 5, 3}, new double[]{1, 2, 3, 1})), - wkt.fromWKT("polygon ((3 1, 4 2, 5 3))")); + wkt.fromWKT("POLYGON ((3 1, 4 2, 5 3))")); assertEquals(new Polygon(new LinearRing(new double[]{3, 4, 5, 3}, new double[]{1, 2, 3, 1}, new double[]{5, 4, 3, 5})), - wkt.fromWKT("polygon ((3 1 5, 4 2 4, 5 3 3))")); + wkt.fromWKT("POLYGON ((3 1 5, 4 2 4, 5 3 3))")); assertEquals(new Polygon(new LinearRing(new double[]{3, 4, 5, 3}, new double[]{1, 2, 3, 1}), Collections.singletonList(new LinearRing(new double[]{0.5, 2.5, 2.0, 0.5}, new double[]{1.5, 1.5, 1.0, 1.5}))), - wkt.fromWKT("polygon ((3 1, 4 2, 5 3, 3 1), (0.5 1.5, 2.5 1.5, 2.0 1.0))")); + wkt.fromWKT("POLYGON ((3 1, 4 2, 5 3, 3 1), (0.5 1.5, 2.5 1.5, 2.0 1.0))")); - assertEquals("polygon EMPTY", wkt.toWKT(Polygon.EMPTY)); - assertEquals(Polygon.EMPTY, wkt.fromWKT("polygon EMPTY)")); + assertEquals("POLYGON EMPTY", wkt.toWKT(Polygon.EMPTY)); + assertEquals(Polygon.EMPTY, wkt.fromWKT("POLYGON EMPTY)")); } public void testInitValidation() { diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/RectangleTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/RectangleTests.java index b613e5b191f19..32ce6abe8de6e 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/RectangleTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/RectangleTests.java @@ -37,11 +37,11 @@ protected Rectangle createTestInstance(boolean hasAlt) { public void testBasicSerialization() throws IOException, ParseException { WellKnownText wkt = new WellKnownText(true, new GeographyValidator(true)); - assertEquals("bbox (10.0, 20.0, 40.0, 30.0)", wkt.toWKT(new Rectangle(10, 20, 40, 30))); - assertEquals(new Rectangle(10, 20, 40, 30), wkt.fromWKT("bbox (10.0, 20.0, 40.0, 30.0)")); + assertEquals("BBOX (10.0, 20.0, 40.0, 30.0)", wkt.toWKT(new Rectangle(10, 20, 40, 30))); + assertEquals(new Rectangle(10, 20, 40, 30), wkt.fromWKT("BBOX (10.0, 20.0, 40.0, 30.0)")); - assertEquals("bbox EMPTY", wkt.toWKT(Rectangle.EMPTY)); - assertEquals(Rectangle.EMPTY, wkt.fromWKT("bbox EMPTY)")); + assertEquals("BBOX EMPTY", wkt.toWKT(Rectangle.EMPTY)); + assertEquals(Rectangle.EMPTY, wkt.fromWKT("BBOX EMPTY)")); } public void testInitValidation() { diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java index fc3dc43294ba0..df4b47f23689f 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java @@ -113,7 +113,7 @@ public void testWKTParsing() throws Exception { XContentBuilder newGeoJson = XContentFactory.jsonBuilder().startObject().field("val"); format.toXContent(new Point(100, 10), newGeoJson, ToXContent.EMPTY_PARAMS); newGeoJson.endObject(); - assertEquals("{\"val\":\"point (100.0 10.0)\"}", Strings.toString(newGeoJson)); + assertEquals("{\"val\":\"POINT (100.0 10.0)\"}", Strings.toString(newGeoJson)); } // Make sure we can parse values outside the normal lat lon boundaries diff --git a/x-pack/plugin/sql/qa/src/main/resources/docs/geo.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/docs/geo.csv-spec index 1307079e216ca..899147fd3e6d9 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/docs/geo.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/docs/geo.csv-spec @@ -15,7 +15,7 @@ selectAsWKT SELECT city, ST_AsWKT(location) location FROM "geo" WHERE city = 'Amsterdam'; city:s | location:s -Amsterdam |point (4.850311987102032 52.347556999884546) +Amsterdam |POINT (4.850311987102032 52.347556999884546) // end::aswkt ; @@ -24,7 +24,7 @@ selectWKTToSQL SELECT CAST(ST_WKTToSQL('POINT (10 20)') AS STRING) location; location:s -point (10.0 20.0) +POINT (10.0 20.0) // end::wkttosql ; diff --git a/x-pack/plugin/sql/qa/src/main/resources/geo/geosql.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/geo/geosql.csv-spec index e63f4325cb0e7..ad31e3dbe9dd3 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/geo/geosql.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/geo/geosql.csv-spec @@ -33,21 +33,21 @@ selectAllPointsAsStrings SELECT city, CAST(location AS STRING) location, CAST(location_no_dv AS STRING) location_no_dv, CAST(geoshape AS STRING) geoshape, CAST(shape AS STRING) shape, region FROM "geo" ORDER BY "city"; city:s | location:s | location_no_dv:s | geoshape:s | shape:s | region:s -Amsterdam |point (4.850311987102032 52.347556999884546) |point (4.850312 52.347557) |point (4.850312 52.347557 2.0) |point (4.850312 52.347557 2.0) |Europe -Berlin |point (13.390888944268227 52.48670099303126) |point (13.390889 52.486701) |point (13.390889 52.486701 34.0) |point (13.390889 52.486701 34.0) |Europe -Chicago |point (-87.63787407428026 41.888782968744636) |point (-87.637874 41.888783) |point (-87.637874 41.888783 181.0) |point (-87.637874 41.888783 181.0) |Americas -Hong Kong |point (114.18392493389547 22.28139698971063) |point (114.183925 22.281397) |point (114.183925 22.281397 552.0) |point (114.183925 22.281397 552.0) |Asia -London |point (-0.12167204171419144 51.51087098289281)|point (-0.121672 51.510871) |point (-0.121672 51.510871 11.0) |point (-0.121672 51.510871 11.0) |Europe -Mountain View |point (-122.08384302444756 37.38648299127817) |point (-122.083843 37.386483) |point (-122.083843 37.386483 30.0) |point (-122.083843 37.386483 30.0) |Americas -Munich |point (11.537504978477955 48.14632098656148) |point (11.537505 48.146321) |point (11.537505 48.146321 519.0) |point (11.537505 48.146321 519.0) |Europe -New York |point (-73.9900270756334 40.74517097789794) |point (-73.990027 40.745171) |point (-73.990027 40.745171 10.0) |point (-73.990027 40.745171 10.0) |Americas -Paris |point (2.3517729341983795 48.84553796611726) |point (2.351773 48.845538) |point (2.351773 48.845538 35.0) |point (2.351773 48.845538 35.0) |Europe -Phoenix |point (-111.97350500151515 33.37624196894467) |point (-111.973505 33.376242) |point (-111.973505 33.376242 331.0)|point (-111.973505 33.376242 331.0)|Americas -San Francisco |point (-122.39422800019383 37.789540970698) |point (-122.394228 37.789541) |point (-122.394228 37.789541 16.0) |point (-122.394228 37.789541 16.0) |Americas -Seoul |point (127.06085099838674 37.50913198571652) |point (127.060851 37.509132) |point (127.060851 37.509132 38.0) |point (127.060851 37.509132 38.0) |Asia -Singapore |point (103.8555349688977 1.2958679627627134) |point (103.855535 1.295868) |point (103.855535 1.295868 15.0) |point (103.855535 1.295868 15.0) |Asia -Sydney |point (151.20862897485495 -33.863385021686554)|point (151.208629 -33.863385) |point (151.208629 -33.863385 100.0)|point (151.208629 -33.863385 100.0)|Asia -Tokyo |point (139.76402222178876 35.66961596254259) |point (139.76402225 35.669616)|point (139.76402225 35.669616 40.0)|point (139.76402225 35.669616 40.0)|Asia +Amsterdam |POINT (4.850311987102032 52.347556999884546) |POINT (4.850312 52.347557) |POINT (4.850312 52.347557 2.0) |POINT (4.850312 52.347557 2.0) |Europe +Berlin |POINT (13.390888944268227 52.48670099303126) |POINT (13.390889 52.486701) |POINT (13.390889 52.486701 34.0) |POINT (13.390889 52.486701 34.0) |Europe +Chicago |POINT (-87.63787407428026 41.888782968744636) |POINT (-87.637874 41.888783) |POINT (-87.637874 41.888783 181.0) |POINT (-87.637874 41.888783 181.0) |Americas +Hong Kong |POINT (114.18392493389547 22.28139698971063) |POINT (114.183925 22.281397) |POINT (114.183925 22.281397 552.0) |POINT (114.183925 22.281397 552.0) |Asia +London |POINT (-0.12167204171419144 51.51087098289281)|POINT (-0.121672 51.510871) |POINT (-0.121672 51.510871 11.0) |POINT (-0.121672 51.510871 11.0) |Europe +Mountain View |POINT (-122.08384302444756 37.38648299127817) |POINT (-122.083843 37.386483) |POINT (-122.083843 37.386483 30.0) |POINT (-122.083843 37.386483 30.0) |Americas +Munich |POINT (11.537504978477955 48.14632098656148) |POINT (11.537505 48.146321) |POINT (11.537505 48.146321 519.0) |POINT (11.537505 48.146321 519.0) |Europe +New York |POINT (-73.9900270756334 40.74517097789794) |POINT (-73.990027 40.745171) |POINT (-73.990027 40.745171 10.0) |POINT (-73.990027 40.745171 10.0) |Americas +Paris |POINT (2.3517729341983795 48.84553796611726) |POINT (2.351773 48.845538) |POINT (2.351773 48.845538 35.0) |POINT (2.351773 48.845538 35.0) |Europe +Phoenix |POINT (-111.97350500151515 33.37624196894467) |POINT (-111.973505 33.376242) |POINT (-111.973505 33.376242 331.0)|POINT (-111.973505 33.376242 331.0)|Americas +San Francisco |POINT (-122.39422800019383 37.789540970698) |POINT (-122.394228 37.789541) |POINT (-122.394228 37.789541 16.0) |POINT (-122.394228 37.789541 16.0) |Americas +Seoul |POINT (127.06085099838674 37.50913198571652) |POINT (127.060851 37.509132) |POINT (127.060851 37.509132 38.0) |POINT (127.060851 37.509132 38.0) |Asia +Singapore |POINT (103.8555349688977 1.2958679627627134) |POINT (103.855535 1.295868) |POINT (103.855535 1.295868 15.0) |POINT (103.855535 1.295868 15.0) |Asia +Sydney |POINT (151.20862897485495 -33.863385021686554)|POINT (151.208629 -33.863385) |POINT (151.208629 -33.863385 100.0)|POINT (151.208629 -33.863385 100.0)|Asia +Tokyo |POINT (139.76402222178876 35.66961596254259) |POINT (139.76402225 35.669616)|POINT (139.76402225 35.669616 40.0)|POINT (139.76402225 35.669616 40.0)|Asia ; // TODO: Both shape and location contain the same data for now, we should change it later to make things more interesting @@ -55,28 +55,28 @@ selectAllPointsAsWKT SELECT city, ST_ASWKT(location) location_wkt, ST_ASWKT(geoshape) geoshape_wkt, region FROM "geo" ORDER BY "city"; city:s | location_wkt:s | geoshape_wkt:s | region:s -Amsterdam |point (4.850311987102032 52.347556999884546) |point (4.850312 52.347557 2.0) |Europe -Berlin |point (13.390888944268227 52.48670099303126) |point (13.390889 52.486701 34.0) |Europe -Chicago |point (-87.63787407428026 41.888782968744636) |point (-87.637874 41.888783 181.0) |Americas -Hong Kong |point (114.18392493389547 22.28139698971063) |point (114.183925 22.281397 552.0) |Asia -London |point (-0.12167204171419144 51.51087098289281)|point (-0.121672 51.510871 11.0) |Europe -Mountain View |point (-122.08384302444756 37.38648299127817) |point (-122.083843 37.386483 30.0) |Americas -Munich |point (11.537504978477955 48.14632098656148) |point (11.537505 48.146321 519.0) |Europe -New York |point (-73.9900270756334 40.74517097789794) |point (-73.990027 40.745171 10.0) |Americas -Paris |point (2.3517729341983795 48.84553796611726) |point (2.351773 48.845538 35.0) |Europe -Phoenix |point (-111.97350500151515 33.37624196894467) |point (-111.973505 33.376242 331.0) |Americas -San Francisco |point (-122.39422800019383 37.789540970698) |point (-122.394228 37.789541 16.0) |Americas -Seoul |point (127.06085099838674 37.50913198571652) |point (127.060851 37.509132 38.0) |Asia -Singapore |point (103.8555349688977 1.2958679627627134) |point (103.855535 1.295868 15.0) |Asia -Sydney |point (151.20862897485495 -33.863385021686554)|point (151.208629 -33.863385 100.0) |Asia -Tokyo |point (139.76402222178876 35.66961596254259) |point (139.76402225 35.669616 40.0) |Asia +Amsterdam |POINT (4.850311987102032 52.347556999884546) |POINT (4.850312 52.347557 2.0) |Europe +Berlin |POINT (13.390888944268227 52.48670099303126) |POINT (13.390889 52.486701 34.0) |Europe +Chicago |POINT (-87.63787407428026 41.888782968744636) |POINT (-87.637874 41.888783 181.0) |Americas +Hong Kong |POINT (114.18392493389547 22.28139698971063) |POINT (114.183925 22.281397 552.0) |Asia +London |POINT (-0.12167204171419144 51.51087098289281)|POINT (-0.121672 51.510871 11.0) |Europe +Mountain View |POINT (-122.08384302444756 37.38648299127817) |POINT (-122.083843 37.386483 30.0) |Americas +Munich |POINT (11.537504978477955 48.14632098656148) |POINT (11.537505 48.146321 519.0) |Europe +New York |POINT (-73.9900270756334 40.74517097789794) |POINT (-73.990027 40.745171 10.0) |Americas +Paris |POINT (2.3517729341983795 48.84553796611726) |POINT (2.351773 48.845538 35.0) |Europe +Phoenix |POINT (-111.97350500151515 33.37624196894467) |POINT (-111.973505 33.376242 331.0) |Americas +San Francisco |POINT (-122.39422800019383 37.789540970698) |POINT (-122.394228 37.789541 16.0) |Americas +Seoul |POINT (127.06085099838674 37.50913198571652) |POINT (127.060851 37.509132 38.0) |Asia +Singapore |POINT (103.8555349688977 1.2958679627627134) |POINT (103.855535 1.295868 15.0) |Asia +Sydney |POINT (151.20862897485495 -33.863385021686554)|POINT (151.208629 -33.863385 100.0) |Asia +Tokyo |POINT (139.76402222178876 35.66961596254259) |POINT (139.76402225 35.669616 40.0) |Asia ; selectWithAsWKTInWhere SELECT city, ST_ASWKT(location) location_wkt, region FROM "geo" WHERE LOCATE('114', ST_ASWKT(location)) > 0 ORDER BY "city"; city:s | location_wkt:s | region:s -Hong Kong |point (114.18392493389547 22.28139698971063)|Asia +Hong Kong |POINT (114.18392493389547 22.28139698971063)|Asia ; selectAllPointsOrderByLonFromAsWKT @@ -112,30 +112,30 @@ selectRegionUsingWktToSql SELECT region, city, ST_ASWKT(ST_WKTTOSQL(region_point)) region_wkt FROM geo ORDER BY region, city; region:s | city:s | region_wkt:s -Americas |Chicago |point (-105.2551 54.526) -Americas |Mountain View |point (-105.2551 54.526) -Americas |New York |point (-105.2551 54.526) -Americas |Phoenix |point (-105.2551 54.526) -Americas |San Francisco |point (-105.2551 54.526) -Asia |Hong Kong |point (100.6197 34.0479) -Asia |Seoul |point (100.6197 34.0479) -Asia |Singapore |point (100.6197 34.0479) -Asia |Sydney |point (100.6197 34.0479) -Asia |Tokyo |point (100.6197 34.0479) -Europe |Amsterdam |point (15.2551 54.526) -Europe |Berlin |point (15.2551 54.526) -Europe |London |point (15.2551 54.526) -Europe |Munich |point (15.2551 54.526) -Europe |Paris |point (15.2551 54.526) +Americas |Chicago |POINT (-105.2551 54.526) +Americas |Mountain View |POINT (-105.2551 54.526) +Americas |New York |POINT (-105.2551 54.526) +Americas |Phoenix |POINT (-105.2551 54.526) +Americas |San Francisco |POINT (-105.2551 54.526) +Asia |Hong Kong |POINT (100.6197 34.0479) +Asia |Seoul |POINT (100.6197 34.0479) +Asia |Singapore |POINT (100.6197 34.0479) +Asia |Sydney |POINT (100.6197 34.0479) +Asia |Tokyo |POINT (100.6197 34.0479) +Europe |Amsterdam |POINT (15.2551 54.526) +Europe |Berlin |POINT (15.2551 54.526) +Europe |London |POINT (15.2551 54.526) +Europe |Munich |POINT (15.2551 54.526) +Europe |Paris |POINT (15.2551 54.526) ; selectCitiesWithAGroupByWktToSql SELECT COUNT(city) city_by_region, CAST(ST_WKTTOSQL(region_point) AS STRING) region FROM geo WHERE city LIKE '%a%' GROUP BY ST_WKTTOSQL(region_point) ORDER BY ST_WKTTOSQL(region_point); city_by_region:l | region:s -3 |point (-105.2551 54.526) -1 |point (100.6197 34.0479) -2 |point (15.2551 54.526) +3 |POINT (-105.2551 54.526) +1 |POINT (100.6197 34.0479) +2 |POINT (15.2551 54.526) ; selectCitiesWithEOrderByWktToSql diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/GeoProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/GeoProcessorTests.java index 07cc6171cf013..b14a61945cf41 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/GeoProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/GeoProcessorTests.java @@ -33,8 +33,8 @@ protected GeoProcessor mutateInstance(GeoProcessor instance) throws IOException } public void testApplyAsWKT() throws Exception { - assertEquals("point (10.0 20.0)", new GeoProcessor(GeoOperation.ASWKT).process(new GeoShape(10, 20))); - assertEquals("point (10.0 20.0)", new GeoProcessor(GeoOperation.ASWKT).process(new GeoShape("POINT (10 20)"))); + assertEquals("POINT (10.0 20.0)", new GeoProcessor(GeoOperation.ASWKT).process(new GeoShape(10, 20))); + assertEquals("POINT (10.0 20.0)", new GeoProcessor(GeoOperation.ASWKT).process(new GeoShape("POINT (10 20)"))); } public void testApplyGeometryType() throws Exception { diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/StWkttosqlProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/StWkttosqlProcessorTests.java index 82a580d159cb8..8c2d61ed80094 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/StWkttosqlProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/StWkttosqlProcessorTests.java @@ -21,7 +21,7 @@ public void testApply() { Object result = proc.process("POINT (10 20)"); assertThat(result, instanceOf(GeoShape.class)); GeoShape geoShape = (GeoShape) result; - assertEquals("point (10.0 20.0)", geoShape.toString()); + assertEquals("POINT (10.0 20.0)", geoShape.toString()); } public void testTypeCheck() { @@ -46,6 +46,6 @@ public void testCoerce() { Object result = proc.process("POLYGON ((3 1 5, 4 2 4, 5 3 3))"); assertThat(result, instanceOf(GeoShape.class)); GeoShape geoShape = (GeoShape) result; - assertEquals("polygon ((3.0 1.0 5.0, 4.0 2.0 4.0, 5.0 3.0 3.0, 3.0 1.0 5.0))", geoShape.toString()); + assertEquals("POLYGON ((3.0 1.0 5.0, 4.0 2.0 4.0, 5.0 3.0 3.0, 3.0 1.0 5.0))", geoShape.toString()); } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java index 41da90ad3164f..0007398fbb83a 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java @@ -309,7 +309,7 @@ private void testDateRangeWithCurrentFunctions(String function, String pattern, assertEquals(operator.equals("=") || operator.equals("!=") || operator.equals(">="), rq.includeLower()); assertEquals(pattern, rq.format()); } - + private void testDateRangeWithCurrentFunctions_AndRangeOptimization(String function, String pattern, ZonedDateTime lowerValue, ZonedDateTime upperValue) { String lowerOperator = randomFrom(new String[] {"<", "<="}); @@ -324,7 +324,7 @@ private void testDateRangeWithCurrentFunctions_AndRangeOptimization(String funct assertEquals(1, eqe.output().size()); assertEquals("test.some.string", eqe.output().get(0).qualifiedName()); assertEquals(DataType.TEXT, eqe.output().get(0).dataType()); - + Query query = eqe.queryContainer().query(); // the range queries optimization should create a single "range" query with "from" and "to" populated with the values // in the two branches of the AND condition @@ -820,7 +820,7 @@ public void testTranslateStWktToSql() { "InternalSqlScriptUtils.eq(InternalSqlScriptUtils.stWktToSql(" + "InternalSqlScriptUtils.docValue(doc,params.v0)),InternalSqlScriptUtils.stWktToSql(params.v1)))", aggFilter.scriptTemplate().toString()); - assertEquals("[{v=keyword}, {v=point (10.0 20.0)}]", aggFilter.scriptTemplate().params().toString()); + assertEquals("[{v=keyword}, {v=POINT (10.0 20.0)}]", aggFilter.scriptTemplate().params().toString()); } public void testTranslateStDistanceToScript() { @@ -840,7 +840,7 @@ public void testTranslateStDistanceToScript() { "InternalSqlScriptUtils.stDistance(" + "InternalSqlScriptUtils.geoDocValue(doc,params.v0),InternalSqlScriptUtils.stWktToSql(params.v1)),params.v2))", sc.script().toString()); - assertEquals("[{v=point}, {v=point (10.0 20.0)}, {v=20}]", sc.script().params().toString()); + assertEquals("[{v=point}, {v=POINT (10.0 20.0)}, {v=20}]", sc.script().params().toString()); } public void testTranslateStDistanceToQuery() { From 2834550dc8f53c710bd8b700da76f39dfd6afb20 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 18 Dec 2019 09:50:31 -0800 Subject: [PATCH 256/686] [DOCS] Move machine learning results definitions into APIs (#50257) --- .../apis/get-bucket.asciidoc | 123 ++++- .../apis/get-category.asciidoc | 60 ++- .../apis/get-influencer.asciidoc | 111 ++-- .../apis/get-overall-buckets.asciidoc | 54 +- .../apis/get-record.asciidoc | 208 ++++++-- .../apis/resultsresource.asciidoc | 479 ------------------ docs/reference/ml/ml-shared.asciidoc | 23 + docs/reference/redirects.asciidoc | 17 + docs/reference/rest-api/defs.asciidoc | 2 - 9 files changed, 471 insertions(+), 606 deletions(-) delete mode 100644 docs/reference/ml/anomaly-detection/apis/resultsresource.asciidoc diff --git a/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc index 027de1385e83f..f06632bcbd54f 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc @@ -40,50 +40,133 @@ bucket. include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: - (Optional, string) The timestamp of a single bucket result. If you do not - specify this parameter, the API returns information about all buckets. +(Optional, string) The timestamp of a single bucket result. If you do not +specify this parameter, the API returns information about all buckets. [[ml-get-bucket-request-body]] ==== {api-request-body-title} `anomaly_score`:: - (Optional, double) Returns buckets with anomaly scores greater or equal than - this value. +(Optional, double) Returns buckets with anomaly scores greater or equal than +this value. `desc`:: - (Optional, boolean) If true, the buckets are sorted in descending order. +(Optional, boolean) If true, the buckets are sorted in descending order. `end`:: - (Optional, string) Returns buckets with timestamps earlier than this time. +(Optional, string) Returns buckets with timestamps earlier than this time. `exclude_interim`:: - (Optional, boolean) If true, the output excludes interim results. By default, - interim results are included. +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=exclude-interim-results] `expand`:: - (Optional, boolean) If true, the output includes anomaly records. +(Optional, boolean) If true, the output includes anomaly records. `page`:: -`from`::: - (Optional, integer) Skips the specified number of buckets. -`size`::: - (Optional, integer) Specifies the maximum number of buckets to obtain. +`page`.`from`::: +(Optional, integer) Skips the specified number of buckets. +`page`.`size`::: +(Optional, integer) Specifies the maximum number of buckets to obtain. `sort`:: - (Optional, string) Specifies the sort field for the requested buckets. By - default, the buckets are sorted by the `timestamp` field. +(Optional, string) Specifies the sort field for the requested buckets. By +default, the buckets are sorted by the `timestamp` field. `start`:: - (Optional, string) Returns buckets with timestamps after this time. +(Optional, string) Returns buckets with timestamps after this time. [[ml-get-bucket-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns an array of bucket objects, which have the following properties: -`buckets`:: - (array) An array of bucket objects. For more information, see - <>. +`anomaly_score`:: +(number) The maximum anomaly score, between 0-100, for any of the bucket +influencers. This is an overall, rate-limited score for the job. All the anomaly +records in the bucket contribute to this score. This value might be updated as +new data is analyzed. + +`bucket_influencers`:: +(array) An array of bucket influencer objects, which have the following +properties: + +`bucket_influencers`.`anomaly_score`::: +(number) A normalized score between 0-100, which is calculated for each bucket +influencer. This score might be updated as newer data is analyzed. + +`bucket_influencers`.`bucket_span`::: +(number) The length of the bucket in seconds. This value matches the `bucket_span` +that is specified in the job. + +`bucket_influencers`.`initial_anomaly_score`::: +(number) The score between 0-100 for each bucket influencer. This score is the +initial value that was calculated at the time the bucket was processed. + +`bucket_influencers`.`influencer_field_name`::: +(string) The field name of the influencer. + +`bucket_influencers`.`influencer_field_value`::: +(string) The field value of the influencer. + +`bucket_influencers`.`is_interim`::: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=is-interim] + +`bucket_influencers`.`job_id`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +`bucket_influencers`.`probability`::: +(number) The probability that the bucket has this behavior, in the range 0 to 1. +This value can be held to a high precision of over 300 decimal places, so the +`anomaly_score` is provided as a human-readable and friendly interpretation of +this. + +`bucket_influencers`.`raw_anomaly_score`::: +(number) Internal. + +`bucket_influencers`.`result_type`::: +(string) Internal. This value is always set to `bucket_influencer`. + +`bucket_influencers`.`timestamp`::: +(date) The start time of the bucket for which these results were calculated. + +`bucket_span`:: +(number) +include::{docdir}/ml/ml-shared.asciidoc[tag=bucket-span-results] + +`event_count`:: +(number) The number of input data records processed in this bucket. + +`initial_anomaly_score`:: +(number) The maximum `anomaly_score` for any of the bucket influencers. This is +the initial value that was calculated at the time the bucket was processed. + +`is_interim`:: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=is-interim] + +`job_id`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +`processing_time_ms`:: +(number) The amount of time, in milliseconds, that it took to analyze the +bucket contents and calculate results. + +`result_type`:: +(string) Internal. This value is always set to `bucket`. + +`timestamp`:: +(date) The start time of the bucket. This timestamp uniquely identifies the +bucket. ++ +-- +NOTE: Events that occur exactly at the timestamp of the bucket are included in +the results for the bucket. + +-- [[ml-get-bucket-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc index 3280b79534f50..92beaa0360b5e 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc @@ -28,7 +28,15 @@ privileges. See <> and [[ml-get-category-desc]] ==== {api-description-title} -For more information about categories, see +When `categorization_field_name` is specified in the job configuration, it is +possible to view the definitions of the resulting categories. A category +definition describes the common terms matched and contains examples of matched +values. + +The anomaly results from a categorization analysis are available as bucket, +influencer, and record results. For example, the results might indicate that +at 16:45 there was an unusual count of log message category 11. You can then +examine the description and examples of that category. For more information, see {stack-ov}/ml-configuring-categories.html[Categorizing log messages]. [[ml-get-category-path-parms]] @@ -39,34 +47,55 @@ For more information about categories, see include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ``:: - (Optional, long) Identifier for the category. If you do not specify this - parameter, the API returns information about all categories in the - {anomaly-job}. +(Optional, long) Identifier for the category. If you do not specify this +parameter, the API returns information about all categories in the {anomaly-job}. [[ml-get-category-request-body]] ==== {api-request-body-title} `page`:: -`from`::: - (Optional, integer) Skips the specified number of categories. -`size`::: - (Optional, integer) Specifies the maximum number of categories to obtain. +`page`.`from`::: +(Optional, integer) Skips the specified number of categories. +`page`.`size`::: +(Optional, integer) Specifies the maximum number of categories to obtain. [[ml-get-category-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns an array of category objects, which have the following properties: -`categories`:: - (array) An array of category objects. For more information, see - <>. +`category_id`:: +(unsigned integer) A unique identifier for the category. + +`examples`:: +(array) A list of examples of actual values that matched the category. + +`grok_pattern`:: +experimental[] (string) A Grok pattern that could be used in {ls} or an ingest +pipeline to extract fields from messages that match the category. This field is experimental and may be changed or removed in a future release. The Grok +patterns that are found are not optimal, but are often a good starting point for +manual tweaking. + +`job_id`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +`max_matching_length`:: +(unsigned integer) The maximum length of the fields that matched the category. +The value is increased by 10% to enable matching for similar fields that have +not been analyzed. + +`regex`:: +(string) A regular expression that is used to search for values that match the +category. + +`terms`:: +(string) A space separated list of the common tokens that are matched in values +of the category. [[ml-get-category-example]] ==== {api-examples-title} -The following example gets information about one category for the -`esxi_log` job: - [source,console] -------------------------------------------------- GET _ml/anomaly_detectors/esxi_log/results/categories @@ -78,7 +107,6 @@ GET _ml/anomaly_detectors/esxi_log/results/categories -------------------------------------------------- // TEST[skip:todo] -In this example, the API returns the following information: [source,js] ---- { diff --git a/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc index 2165d8ef9f7f9..e2727a04dc07c 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc @@ -23,6 +23,13 @@ need `read` index privilege on the index that stores the results. The privileges. See <> and <>. +[[ml-get-influencer-desc]] +==== {api-description-title} + +Influencers are the entities that have contributed to, or are to blame for, +the anomalies. Influencer results are available only if an +`influencer_field_name` is specified in the job configuration. + [[ml-get-influencer-path-parms]] ==== {api-path-parms-title} @@ -34,75 +41,119 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ==== {api-request-body-title} `desc`:: - (Optional, boolean) If true, the results are sorted in descending order. +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=desc-results] `end`:: - (Optional, string) Returns influencers with timestamps earlier than this time. +(Optional, string) Returns influencers with timestamps earlier than this time. `exclude_interim`:: - (Optional, boolean) If true, the output excludes interim results. By default, - interim results are included. +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=exclude-interim-results] `influencer_score`:: - (Optional, double) Returns influencers with anomaly scores greater than or - equal to this value. +(Optional, double) Returns influencers with anomaly scores greater than or +equal to this value. `page`:: `from`::: - (Optional, integer) Skips the specified number of influencers. +(Optional, integer) Skips the specified number of influencers. `size`::: - (Optional, integer) Specifies the maximum number of influencers to obtain. +(Optional, integer) Specifies the maximum number of influencers to obtain. `sort`:: - (Optional, string) Specifies the sort field for the requested influencers. By - default, the influencers are sorted by the `influencer_score` value. +(Optional, string) Specifies the sort field for the requested influencers. By +default, the influencers are sorted by the `influencer_score` value. `start`:: - (Optional, string) Returns influencers with timestamps after this time. +(Optional, string) Returns influencers with timestamps after this time. [[ml-get-influencer-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns an array of influencer objects, which have the following +properties: + +`bucket_span`:: +(number) +include::{docdir}/ml/ml-shared.asciidoc[tag=bucket-span-results] + +`influencer_score`:: +(number) A normalized score between 0-100, which is based on the probability of +the influencer in this bucket aggregated across detectors. Unlike +`initial_influencer_score`, this value will be updated by a re-normalization +process as new data is analyzed. + +`influencer_field_name`:: +(string) The field name of the influencer. + +`influencer_field_value`:: +(string) The entity that influenced, contributed to, or was to blame for the +anomaly. + +`initial_influencer_score`:: +(number) A normalized score between 0-100, which is based on the probability of +the influencer aggregated across detectors. This is the initial value that was +calculated at the time the bucket was processed. -`influencers`:: - (array) An array of influencer objects. - For more information, see <>. +`is_interim`:: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=is-interim] + +`job_id`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +`probability`:: +(number) The probability that the influencer has this behavior, in the range 0 +to 1. For example, 0.0000109783. This value can be held to a high precision of +over 300 decimal places, so the `influencer_score` is provided as a +human-readable and friendly interpretation of this. + +`result_type`:: +(string) Internal. This value is always set to `influencer`. + +`timestamp`:: +(date) +include::{docdir}/ml/ml-shared.asciidoc[tag=timestamp-results] + +NOTE: Additional influencer properties are added, depending on the fields being +analyzed. For example, if it's analyzing `user_name` as an influencer, then a +field `user_name` is added to the result document. This information enables you to +filter the anomaly results more easily. [[ml-get-influencer-example]] ==== {api-examples-title} -The following example gets influencer information for the `it_ops_new_kpi` job: - [source,console] -------------------------------------------------- -GET _ml/anomaly_detectors/it_ops_new_kpi/results/influencers +GET _ml/anomaly_detectors/high_sum_total_sales/results/influencers { "sort": "influencer_score", "desc": true } -------------------------------------------------- -// TEST[skip:todo] +// TEST[skip:Kibana sample data] In this example, the API returns the following information, sorted based on the influencer score in descending order: [source,js] ---- { - "count": 28, + "count": 189, "influencers": [ { - "job_id": "it_ops_new_kpi", + "job_id": "high_sum_total_sales", "result_type": "influencer", - "influencer_field_name": "kpi_indicator", - "influencer_field_value": "online_purchases", - "kpi_indicator": "online_purchases", - "influencer_score": 94.1386, - "initial_influencer_score": 94.1386, - "probability": 0.000111612, - "bucket_span": 600, - "is_interim": false, - "timestamp": 1454943600000 + "influencer_field_name": "customer_full_name.keyword", + "influencer_field_value": "Wagdi Shaw", + "customer_full_name.keyword" : "Wagdi Shaw", + "influencer_score": 99.02493, + "initial_influencer_score" : 94.67233079580171, + "probability" : 1.4784807245686567E-10, + "bucket_span" : 3600, + "is_interim" : false, + "timestamp" : 1574661600000 }, ... ] diff --git a/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc index 62acd7902b9ff..3c0df917e4b3c 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc @@ -54,6 +54,8 @@ a span equal to the jobs' largest bucket span. [[ml-get-overall-buckets-path-parms]] ==== {api-path-parms-title} +``:: +(Required, string) include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-wildcard-list] [[ml-get-overall-buckets-request-body]] @@ -64,38 +66,56 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-wildcard-li include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-jobs] `bucket_span`:: - (Optional, string) The span of the overall buckets. Must be greater or equal - to the largest bucket span of the specified {anomaly-jobs}, which is the - default value. +(Optional, string) The span of the overall buckets. Must be greater or equal to +the largest bucket span of the specified {anomaly-jobs}, which is the default +value. `end`:: - (Optional, string) Returns overall buckets with timestamps earlier than this - time. +(Optional, string) Returns overall buckets with timestamps earlier than this +time. `exclude_interim`:: - (Optional, boolean) If `true`, the output excludes interim overall buckets. - Overall buckets are interim if any of the job buckets within the overall - bucket interval are interim. By default, interim results are included. +(Optional, boolean) If `true`, the output excludes interim overall buckets. +Overall buckets are interim if any of the job buckets within the overall bucket +interval are interim. By default, interim results are included. `overall_score`:: - (Optional, double) Returns overall buckets with overall scores greater or - equal than this value. +(Optional, double) Returns overall buckets with overall scores greater or equal +than this value. `start`:: - (Optional, string) Returns overall buckets with timestamps after this time. +(Optional, string) Returns overall buckets with timestamps after this time. `top_n`:: - (Optional, integer) The number of top {anomaly-job} bucket scores to be used - in the `overall_score` calculation. The default value is `1`. +(Optional, integer) The number of top {anomaly-job} bucket scores to be used in +the `overall_score` calculation. The default value is `1`. [[ml-get-overall-buckets-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns an array of overall bucket objects, which have the following +properties: -`overall_buckets`:: - (array) An array of overall bucket objects. For more information, see - <>. +`bucket_span`:: +(number) The length of the bucket in seconds. Matches the `bucket_span` +of the job with the longest one. + +`is_interim`:: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=is-interim] + +`jobs`:: +(array) An array of objects that contain the `max_anomaly_score` per `job_id`. + +`overall_score`:: +(number) The `top_n` average of the max bucket `anomaly_score` per job. + +`result_type`:: +(string) Internal. This is always set to `overall_bucket`. + +`timestamp`:: +(date) +include::{docdir}/ml/ml-shared.asciidoc[tag=timestamp-results] [[ml-get-overall-buckets-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc index b5bbb15580e19..7e56fc757cb79 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc @@ -22,6 +22,22 @@ need `read` index privilege on the index that stores the results. The `machine_learning_admin` and `machine_learning_user` roles provide these privileges. See <> and <>. +[[ml-get-record-desc]] +==== {api-description-title} + +Records contain the detailed analytical results. They describe the anomalous +activity that has been identified in the input data based on the detector +configuration. + +There can be many anomaly records depending on the characteristics and size of +the input data. In practice, there are often too many to be able to manually +process them. The {ml-features} therefore perform a sophisticated aggregation of +the anomaly records into buckets. + +The number of record results depends on the number of anomalies found in each +bucket, which relates to the number of time series being modeled and the number +of detectors. + [[ml-get-record-path-parms]] ==== {api-path-parms-title} @@ -33,83 +49,191 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] ==== {api-request-body-title} `desc`:: - (Optional, boolean) If true, the results are sorted in descending order. +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=desc-results] `end`:: - (Optional, string) Returns records with timestamps earlier than this time. +(Optional, string) Returns records with timestamps earlier than this time. `exclude_interim`:: - (Optional, boolean) If true, the output excludes interim results. By default, - interim results are included. +(Optional, boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=exclude-interim-results] `page`:: -`from`::: - (Optional, integer) Skips the specified number of records. -`size`::: - (Optional, integer) Specifies the maximum number of records to obtain. +`page`.`from`::: +(Optional, integer) Skips the specified number of records. +`page`.`size`::: +(Optional, integer) Specifies the maximum number of records to obtain. `record_score`:: - (Optional, double) Returns records with anomaly scores greater or equal than - this value. +(Optional, double) Returns records with anomaly scores greater or equal than +this value. `sort`:: - (Optional, string) Specifies the sort field for the requested records. By - default, the records are sorted by the `anomaly_score` value. +(Optional, string) Specifies the sort field for the requested records. By +default, the records are sorted by the `anomaly_score` value. `start`:: - (Optional, string) Returns records with timestamps after this time. +(Optional, string) Returns records with timestamps after this time. [[ml-get-record-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns an array of record objects, which have the following properties: + +`actual`:: +(array) The actual value for the bucket. + +`bucket_span`:: +(number) +include::{docdir}/ml/ml-shared.asciidoc[tag=bucket-span-results] + +`by_field_name`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=by-field-name] + +`by_field_value`:: +(string) The value of `by_field_name`. + +`causes`:: +(array) For population analysis, an over field must be specified in the detector. +This property contains an array of anomaly records that are the causes for the +anomaly that has been identified for the over field. If no over fields exist, +this field is not present. This sub-resource contains the most anomalous records +for the `over_field_name`. For scalability reasons, a maximum of the 10 most +significant causes of the anomaly are returned. As part of the core analytical modeling, these low-level anomaly records are aggregated for their parent over +field record. The causes resource contains similar elements to the record +resource, namely `actual`, `typical`, `geo_results.actual_point`, +`geo_results.typical_point`, `*_field_name` and `*_field_value`. Probability and +scores are not applicable to causes. + +`detector_index`:: +(number) A unique identifier for the detector. + +`field_name`:: +(string) Certain functions require a field to operate on, for example, `sum()`. +For those functions, this value is the name of the field to be analyzed. + +`function`:: +(string) The function in which the anomaly occurs, as specified in the detector +configuration. For example, `max`. + +`function_description`:: +(string) The description of the function in which the anomaly occurs, as +specified in the detector configuration. + +`geo_results.actual_point`:: +(string) The actual value for the bucket formatted as a `geo_point`. If the +detector function is `lat_long`, this is a comma delimited string of the +latitude and longitude. + +`geo_results.typical_point`:: +(string) The typical value for the bucket formatted as a `geo_point`. If the +detector function is `lat_long`, this is a comma delimited string of the +latitude and longitude. + +`influencers`:: +(array) If `influencers` was specified in the detector configuration, this array +contains influencers that contributed to or were to blame for an anomaly. + +`initial_record_score`:: +(number) A normalized score between 0-100, which is based on the probability of +the anomalousness of this record. This is the initial value that was calculated +at the time the bucket was processed. + +`is_interim`:: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=is-interim] + +`job_id`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +`over_field_name`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=over-field-name] + +`over_field_value`:: +(string) The value of `over_field_name`. + +`partition_field_name`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=partition-field-name] + +`partition_field_value`:: +(string) The value of `partition_field_name`. + +`probability`:: +(number) The probability of the individual anomaly occurring, in the range +0 to 1. For example, 0.0000772031. This value can be held to a high precision +of over 300 decimal places, so the `record_score` is provided as a +human-readable and friendly interpretation of this. + +`multi_bucket_impact`:: +(number) An indication of how strongly an anomaly is multi bucket or single +bucket. The value is on a scale of `-5.0` to `+5.0` where `-5.0` means the +anomaly is purely single bucket and `+5.0` means the anomaly is purely multi +bucket. + +`record_score`:: +(number) A normalized score between 0-100, which is based on the probability of +the anomalousness of this record. Unlike `initial_record_score`, this value will +be updated by a re-normalization process as new data is analyzed. + +`result_type`:: +(string) Internal. This is always set to `record`. + +`timestamp`:: +(date) +include::{docdir}/ml/ml-shared.asciidoc[tag=timestamp-results] + +`typical`:: +(array) The typical value for the bucket, according to analytical modeling. + +NOTE: Additional record properties are added, depending on the fields being +analyzed. For example, if it's analyzing `hostname` as a _by field_, then a field +`hostname` is added to the result document. This information enables you to +filter the anomaly results more easily. -`records`:: - (array) An array of record objects. For more information, see - <>. [[ml-get-record-example]] ==== {api-examples-title} -The following example gets record information for the `it-ops-kpi` job: - [source,console] -------------------------------------------------- -GET _ml/anomaly_detectors/it-ops-kpi/results/records +GET _ml/anomaly_detectors/low_request_rate/results/records { "sort": "record_score", "desc": true, "start": "1454944100000" } -------------------------------------------------- -// TEST[skip:todo] +// TEST[skip:Kibana sample data] -In this example, the API returns twelve results for the specified -time constraints: [source,js] ---- { - "count": 12, - "records": [ + "count" : 4, + "records" : [ { - "job_id": "it-ops-kpi", - "result_type": "record", - "probability": 0.00000332668, - "record_score": 72.9929, - "initial_record_score": 65.7923, - "bucket_span": 300, - "detector_index": 0, - "is_interim": false, - "timestamp": 1454944200000, - "function": "low_sum", - "function_description": "sum", - "typical": [ - 1806.48 - ], - "actual": [ - 288 + "job_id" : "low_request_rate", + "result_type" : "record", + "probability" : 1.3882308899968812E-4, + "multi_bucket_impact" : -5.0, + "record_score" : 94.98554565630553, + "initial_record_score" : 94.98554565630553, + "bucket_span" : 3600, + "detector_index" : 0, + "is_interim" : false, + "timestamp" : 1577793600000, + "function" : "low_count", + "function_description" : "count", + "typical" : [ + 28.254208230188834 ], - "field_name": "events_per_min" + "actual" : [ + 0.0 + ] }, ... ] diff --git a/docs/reference/ml/anomaly-detection/apis/resultsresource.asciidoc b/docs/reference/ml/anomaly-detection/apis/resultsresource.asciidoc deleted file mode 100644 index b35100c24e6ae..0000000000000 --- a/docs/reference/ml/anomaly-detection/apis/resultsresource.asciidoc +++ /dev/null @@ -1,479 +0,0 @@ -[role="xpack"] -[testenv="platinum"] -[[ml-results-resource]] -=== Results resources - -Several different result types are created for each job. You can query anomaly -results for _buckets_, _influencers_, and _records_ by using the results API. -Summarized bucket results over multiple jobs can be queried as well; those -results are called _overall buckets_. - -Results are written for each `bucket_span`. The timestamp for the results is the -start of the bucket time interval. - -The results include scores, which are calculated for each anomaly result type and -each bucket interval. These scores are aggregated in order to reduce noise, and -normalized in order to identify and rank the most mathematically significant -anomalies. - -Bucket results provide the top level, overall view of the job and are ideal for -alerts. For example, the bucket results might indicate that at 16:05 the system -was unusual. This information is a summary of all the anomalies, pinpointing -when they occurred. - -Influencer results show which entities were anomalous and when. For example, -the influencer results might indicate that at 16:05 `user_name: Bob` was unusual. -This information is a summary of all the anomalies for each entity, so there -can be a lot of these results. Once you have identified a notable bucket time, -you can look to see which entities were significant. - -Record results provide details about what the individual anomaly was, when it -occurred and which entity was involved. For example, the record results might -indicate that at 16:05 Bob sent 837262434 bytes, when the typical value was -1067 bytes. Once you have identified a bucket time and perhaps a significant -entity too, you can drill through to the record results in order to investigate -the anomalous behavior. - -Categorization results contain the definitions of _categories_ that have been -identified. These are only applicable for jobs that are configured to analyze -unstructured log data using categorization. These results do not contain a -timestamp or any calculated scores. For more information, see -{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. - -* <> -* <> -* <> -* <> -* <> - -NOTE: All of these resources and properties are informational; you cannot -change their values. - -[float] -[[ml-results-buckets]] -==== Buckets - -Bucket results provide the top level, overall view of the job and are best for -alerting. - -Each bucket has an `anomaly_score`, which is a statistically aggregated and -normalized view of the combined anomalousness of all the record results within -each bucket. - -One bucket result is written for each `bucket_span` for each job, even if it is -not considered to be anomalous. If the bucket is not anomalous, it has an -`anomaly_score` of zero. - -When you identify an anomalous bucket, you can investigate further by expanding -the bucket resource to show the records as nested objects. Alternatively, you -can access the records resource directly and filter by the date range. - -A bucket resource has the following properties: - -`anomaly_score`:: - (number) The maximum anomaly score, between 0-100, for any of the bucket - influencers. This is an overall, rate-limited score for the job. All the - anomaly records in the bucket contribute to this score. This value might be - updated as new data is analyzed. - -`bucket_influencers`:: - (array) An array of bucket influencer objects. - For more information, see <>. - -`bucket_span`:: - (number) The length of the bucket in seconds. - This value matches the `bucket_span` that is specified in the job. - -`event_count`:: - (number) The number of input data records processed in this bucket. - -`initial_anomaly_score`:: - (number) The maximum `anomaly_score` for any of the bucket influencers. - This is the initial value that was calculated at the time the bucket was - processed. - -`is_interim`:: - (boolean) If true, this is an interim result. In other words, the bucket - results are calculated based on partial input data. - -`job_id`:: - (string) The unique identifier for the job that these results belong to. - -`processing_time_ms`:: - (number) The amount of time, in milliseconds, that it took to analyze the - bucket contents and calculate results. - -`result_type`:: - (string) Internal. This value is always set to `bucket`. - -`timestamp`:: - (date) The start time of the bucket. This timestamp uniquely identifies the - bucket. + - -NOTE: Events that occur exactly at the timestamp of the bucket are included in -the results for the bucket. - - -[float] -[[ml-results-bucket-influencers]] -==== Bucket Influencers - -Bucket influencer results are available as nested objects contained within -bucket results. These results are an aggregation for each type of influencer. -For example, if both `client_ip` and `user_name` were specified as influencers, -then you would be able to determine when the `client_ip` or `user_name` values -were collectively anomalous. - -There is a built-in bucket influencer called `bucket_time` which is always -available. This bucket influencer is the aggregation of all records in the -bucket; it is not just limited to a type of influencer. - -NOTE: A bucket influencer is a type of influencer. For example, `client_ip` or -`user_name` can be bucket influencers, whereas `192.168.88.2` and `Bob` are -influencers. - -An bucket influencer object has the following properties: - -`anomaly_score`:: - (number) A normalized score between 0-100, which is calculated for each bucket - influencer. This score might be updated as newer data is analyzed. - -`bucket_span`:: - (number) The length of the bucket in seconds. This value matches the `bucket_span` - that is specified in the job. - -`initial_anomaly_score`:: - (number) The score between 0-100 for each bucket influencer. This score is - the initial value that was calculated at the time the bucket was processed. - -`influencer_field_name`:: - (string) The field name of the influencer. For example `client_ip` or - `user_name`. - -`influencer_field_value`:: - (string) The field value of the influencer. For example `192.168.88.2` or - `Bob`. - -`is_interim`:: - (boolean) If true, this is an interim result. In other words, the bucket - influencer results are calculated based on partial input data. - -`job_id`:: - (string) The unique identifier for the job that these results belong to. - -`probability`:: - (number) The probability that the bucket has this behavior, in the range 0 - to 1. For example, 0.0000109783. This value can be held to a high precision - of over 300 decimal places, so the `anomaly_score` is provided as a - human-readable and friendly interpretation of this. - -`raw_anomaly_score`:: - (number) Internal. - -`result_type`:: - (string) Internal. This value is always set to `bucket_influencer`. - -`timestamp`:: - (date) The start time of the bucket for which these results were calculated. - -[float] -[[ml-results-influencers]] -==== Influencers - -Influencers are the entities that have contributed to, or are to blame for, -the anomalies. Influencer results are available only if an -`influencer_field_name` is specified in the job configuration. - -Influencers are given an `influencer_score`, which is calculated based on the -anomalies that have occurred in each bucket interval. For jobs with more than -one detector, this gives a powerful view of the most anomalous entities. - -For example, if you are analyzing unusual bytes sent and unusual domains -visited and you specified `user_name` as the influencer, then an -`influencer_score` for each anomalous user name is written per bucket. For -example, if `user_name: Bob` had an `influencer_score` greater than 75, then -`Bob` would be considered very anomalous during this time interval in one or -both of those areas (unusual bytes sent or unusual domains visited). - -One influencer result is written per bucket for each influencer that is -considered anomalous. - -When you identify an influencer with a high score, you can investigate further -by accessing the records resource for that bucket and enumerating the anomaly -records that contain the influencer. - -An influencer object has the following properties: - -`bucket_span`:: - (number) The length of the bucket in seconds. This value matches the `bucket_span` - that is specified in the job. - -`influencer_score`:: - (number) A normalized score between 0-100, which is based on the probability - of the influencer in this bucket aggregated across detectors. Unlike - `initial_influencer_score`, this value will be updated by a re-normalization - process as new data is analyzed. - -`initial_influencer_score`:: - (number) A normalized score between 0-100, which is based on the probability - of the influencer aggregated across detectors. This is the initial value that - was calculated at the time the bucket was processed. - -`influencer_field_name`:: - (string) The field name of the influencer. - -`influencer_field_value`:: - (string) The entity that influenced, contributed to, or was to blame for the - anomaly. - -`is_interim`:: - (boolean) If true, this is an interim result. In other words, the influencer - results are calculated based on partial input data. - -`job_id`:: - (string) The unique identifier for the job that these results belong to. - -`probability`:: - (number) The probability that the influencer has this behavior, in the range - 0 to 1. For example, 0.0000109783. This value can be held to a high precision - of over 300 decimal places, so the `influencer_score` is provided as a - human-readable and friendly interpretation of this. -// For example, 0.03 means 3%. This value is held to a high precision of over -//300 decimal places. In scientific notation, a value of 3.24E-300 is highly -//unlikely and therefore highly anomalous. - -`result_type`:: - (string) Internal. This value is always set to `influencer`. - -`timestamp`:: - (date) The start time of the bucket for which these results were calculated. - -NOTE: Additional influencer properties are added, depending on the fields being -analyzed. For example, if it's analyzing `user_name` as an influencer, then a -field `user_name` is added to the result document. This information enables you to -filter the anomaly results more easily. - - -[float] -[[ml-results-records]] -==== Records - -Records contain the detailed analytical results. They describe the anomalous -activity that has been identified in the input data based on the detector -configuration. - -For example, if you are looking for unusually large data transfers, an anomaly -record can identify the source IP address, the destination, the time window -during which it occurred, the expected and actual size of the transfer, and the -probability of this occurrence. - -There can be many anomaly records depending on the characteristics and size of -the input data. In practice, there are often too many to be able to manually -process them. The {ml-features} therefore perform a sophisticated -aggregation of the anomaly records into buckets. - -The number of record results depends on the number of anomalies found in each -bucket, which relates to the number of time series being modeled and the number of -detectors. - -A record object has the following properties: - -`actual`:: - (array) The actual value for the bucket. - -`bucket_span`:: - (number) The length of the bucket in seconds. - This value matches the `bucket_span` that is specified in the job. - -`by_field_name`:: - (string) The name of the analyzed field. This value is present only if - it is specified in the detector. For example, `client_ip`. - -`by_field_value`:: - (string) The value of `by_field_name`. This value is present only if - it is specified in the detector. For example, `192.168.66.2`. - -`causes`:: - (array) For population analysis, an over field must be specified in the - detector. This property contains an array of anomaly records that are the - causes for the anomaly that has been identified for the over field. If no - over fields exist, this field is not present. This sub-resource contains - the most anomalous records for the `over_field_name`. For scalability reasons, - a maximum of the 10 most significant causes of the anomaly are returned. As - part of the core analytical modeling, these low-level anomaly records are - aggregated for their parent over field record. The causes resource contains - similar elements to the record resource, namely `actual`, `typical`, - `geo_results.actual_point`, `geo_results.typical_point`, - `*_field_name` and `*_field_value`. - Probability and scores are not applicable to causes. - -`detector_index`:: - (number) A unique identifier for the detector. - -`field_name`:: - (string) Certain functions require a field to operate on, for example, `sum()`. - For those functions, this value is the name of the field to be analyzed. - -`function`:: - (string) The function in which the anomaly occurs, as specified in the - detector configuration. For example, `max`. - -`function_description`:: - (string) The description of the function in which the anomaly occurs, as - specified in the detector configuration. - -`influencers`:: - (array) If `influencers` was specified in the detector configuration, then - this array contains influencers that contributed to or were to blame for an - anomaly. - -`initial_record_score`:: - (number) A normalized score between 0-100, which is based on the - probability of the anomalousness of this record. This is the initial value - that was calculated at the time the bucket was processed. - -`is_interim`:: - (boolean) If true, this is an interim result. In other words, the anomaly - record is calculated based on partial input data. - -`job_id`:: - (string) The unique identifier for the job that these results belong to. - -`over_field_name`:: - (string) The name of the over field that was used in the analysis. This value - is present only if it was specified in the detector. Over fields are used - in population analysis. For example, `user`. - -`over_field_value`:: - (string) The value of `over_field_name`. This value is present only if it - was specified in the detector. For example, `Bob`. - -`partition_field_name`:: - (string) The name of the partition field that was used in the analysis. This - value is present only if it was specified in the detector. For example, - `region`. - -`partition_field_value`:: - (string) The value of `partition_field_name`. This value is present only if - it was specified in the detector. For example, `us-east-1`. - -`probability`:: - (number) The probability of the individual anomaly occurring, in the range - 0 to 1. For example, 0.0000772031. This value can be held to a high precision - of over 300 decimal places, so the `record_score` is provided as a - human-readable and friendly interpretation of this. -//In scientific notation, a value of 3.24E-300 is highly unlikely and therefore -//highly anomalous. - -`multi_bucket_impact`:: - (number) an indication of how strongly an anomaly is multi bucket or single bucket. - The value is on a scale of -5 to +5 where -5 means the anomaly is purely single - bucket and +5 means the anomaly is purely multi bucket. - -`record_score`:: - (number) A normalized score between 0-100, which is based on the probability - of the anomalousness of this record. Unlike `initial_record_score`, this - value will be updated by a re-normalization process as new data is analyzed. - -`result_type`:: - (string) Internal. This is always set to `record`. - -`timestamp`:: - (date) The start time of the bucket for which these results were calculated. - -`typical`:: - (array) The typical value for the bucket, according to analytical modeling. - -`geo_results.actual_point`:: - (string) The actual value for the bucket formatted as a `geo_point`. - If the detector function is `lat_long`, this is a comma delimited string - of the latitude and longitude. - -`geo_results.typical_point`:: - (string) The typical value for the bucket formatted as a `geo_point`. - If the detector function is `lat_long`, this is a comma delimited string - of the latitude and longitude. - -NOTE: Additional record properties are added, depending on the fields being -analyzed. For example, if it's analyzing `hostname` as a _by field_, then a field -`hostname` is added to the result document. This information enables you to -filter the anomaly results more easily. - - -[float] -[[ml-results-categories]] -==== Categories - -When `categorization_field_name` is specified in the job configuration, it is -possible to view the definitions of the resulting categories. A category -definition describes the common terms matched and contains examples of matched -values. - -The anomaly results from a categorization analysis are available as bucket, -influencer, and record results. For example, the results might indicate that -at 16:45 there was an unusual count of log message category 11. You can then -examine the description and examples of that category. - -A category resource has the following properties: - -`category_id`:: - (unsigned integer) A unique identifier for the category. - -`examples`:: - (array) A list of examples of actual values that matched the category. - -`grok_pattern`:: - experimental[] (string) A Grok pattern that could be used in Logstash or an - Ingest Pipeline to extract fields from messages that match the category. This - field is experimental and may be changed or removed in a future release. The - Grok patterns that are found are not optimal, but are often a good starting - point for manual tweaking. - -`job_id`:: - (string) The unique identifier for the job that these results belong to. - -`max_matching_length`:: - (unsigned integer) The maximum length of the fields that matched the category. - The value is increased by 10% to enable matching for similar fields that have - not been analyzed. - -`regex`:: - (string) A regular expression that is used to search for values that match the - category. - -`terms`:: - (string) A space separated list of the common tokens that are matched in - values of the category. - -[float] -[[ml-results-overall-buckets]] -==== Overall Buckets - -Overall buckets provide a summary of bucket results over multiple jobs. -Their `bucket_span` equals the longest `bucket_span` of the jobs in question. -The `overall_score` is the `top_n` average of the max `anomaly_score` per job -within the overall bucket time interval. -This means that you can fine-tune the `overall_score` so that it is more -or less sensitive to the number of jobs that detect an anomaly at the same time. - -An overall bucket resource has the following properties: - -`timestamp`:: - (date) The start time of the overall bucket. - -`bucket_span`:: - (number) The length of the bucket in seconds. Matches the `bucket_span` - of the job with the longest one. - -`overall_score`:: - (number) The `top_n` average of the max bucket `anomaly_score` per job. - -`jobs`:: - (array) An array of objects that contain the `max_anomaly_score` per `job_id`. - -`is_interim`:: - (boolean) If true, this is an interim result. In other words, the anomaly - record is calculated based on partial input data. - -`result_type`:: - (string) Internal. This is always set to `overall_bucket`. diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index 5056281642bae..5003f8c4f5403 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -185,6 +185,11 @@ The size of the interval that the analysis is aggregated into, typically between see <>. end::bucket-span[] +tag::bucket-span-results[] +The length of the bucket in seconds. This value matches the `bucket_span` +that is specified in the job. +end::bucket-span-results[] + tag::by-field-name[] The field used to split the data. In particular, this property is used for analyzing the splits with respect to their own history. It is used for finding @@ -525,6 +530,10 @@ that document will not be used for training, but a prediction with the trained model will be generated for it. It is also known as continuous target variable. end::dependent-variable[] +tag::desc-results[] +If true, the results are sorted in descending order. +end::desc-results[] + tag::description-dfa[] A description of the job. end::description-dfa[] @@ -623,6 +632,11 @@ working with both over and by fields, then you can set `exclude_frequent` to `all` for both fields, or to `by` or `over` for those specific fields. end::exclude-frequent[] +tag::exclude-interim-results[] +If `true`, the output excludes interim results. By default, interim results are +included. +end::exclude-interim-results[] + tag::feature-bag-fraction[] Defines the fraction of features that will be used when selecting a random bag for each candidate split. @@ -718,6 +732,11 @@ is available as part of the input data. When you use multiple detectors, the use of influencers is recommended as it aggregates results for each influencer entity. end::influencers[] +tag::is-interim[] +If `true`, this is an interim result. In other words, the results are calculated +based on partial input data. +end::is-interim[] + tag::job-id-anomaly-detection[] Identifier for the {anomaly-job}. end::job-id-anomaly-detection[] @@ -1129,6 +1148,10 @@ The time span that each search will be querying. This setting is only applicable when the mode is set to `manual`. For example: `3h`. end::time-span[] +tag::timestamp-results[] +The start time of the bucket for which these results were calculated. +end::timestamp-results[] + tag::tokenizer[] The name or definition of the <> to use after character filters are applied. This property is compulsory if diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 32a30a326bfde..57c7bf93092a7 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -1095,3 +1095,20 @@ See <> and <>. This page was deleted. See <>, <>, <>, <>. + +[role="exclude",id="ml-results-resource"] +=== Results resources + +This page was deleted. +[[ml-results-buckets]] +See <>, +[[ml-results-bucket-influencers]] +<>, +[[ml-results-influencers]] +<>, +[[ml-results-records]] +<>, +[[ml-results-categories]] +<>, and +[[ml-results-overall-buckets]] +<>. diff --git a/docs/reference/rest-api/defs.asciidoc b/docs/reference/rest-api/defs.asciidoc index 137af880c0a9a..85ffbae5925fd 100644 --- a/docs/reference/rest-api/defs.asciidoc +++ b/docs/reference/rest-api/defs.asciidoc @@ -7,10 +7,8 @@ These resource definitions are used in APIs related to {ml-features} and * <> -* <> * <> include::{es-repo-dir}/ml/df-analytics/apis/analysisobjects.asciidoc[] include::{xes-repo-dir}/rest-api/security/role-mapping-resources.asciidoc[] -include::{es-repo-dir}/ml/anomaly-detection/apis/resultsresource.asciidoc[] From c4fdc67f5e1b75e869665d68a1fbde89649c220f Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Wed, 18 Dec 2019 18:41:26 +0100 Subject: [PATCH 257/686] Add 7.5.2 version. --- .ci/bwcVersions | 1 + server/src/main/java/org/elasticsearch/Version.java | 1 + 2 files changed, 2 insertions(+) diff --git a/.ci/bwcVersions b/.ci/bwcVersions index 52b778bf11235..e94365d6b0a22 100644 --- a/.ci/bwcVersions +++ b/.ci/bwcVersions @@ -13,5 +13,6 @@ BWC_VERSION: - "7.4.2" - "7.5.0" - "7.5.1" + - "7.5.2" - "7.6.0" - "8.0.0" diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index c39502f0b0e3d..ce8d0053e76dc 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -71,6 +71,7 @@ public class Version implements Comparable, ToXContentFragment { public static final Version V_7_4_2 = new Version(7040299, org.apache.lucene.util.Version.LUCENE_8_2_0); public static final Version V_7_5_0 = new Version(7050099, org.apache.lucene.util.Version.LUCENE_8_3_0); public static final Version V_7_5_1 = new Version(7050199, org.apache.lucene.util.Version.LUCENE_8_3_0); + public static final Version V_7_5_2 = new Version(7050299, org.apache.lucene.util.Version.LUCENE_8_3_0); public static final Version V_7_6_0 = new Version(7060099, org.apache.lucene.util.Version.LUCENE_8_4_0); public static final Version V_8_0_0 = new Version(8000099, org.apache.lucene.util.Version.LUCENE_8_4_0); public static final Version CURRENT = V_8_0_0; From ffbd22ee68db0b559bd843af8f3b0b2fdef42407 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Wed, 18 Dec 2019 14:03:41 -0500 Subject: [PATCH 258/686] [ML][Inference] fix support for nested fields (#50258) This fixes support for nested fields We now support fully nested, fully collapsed, or a mix of both on inference docs. ES mappings allow the `_source` to be any combination of nested objects + dot delimited fields. So, we should do our best to find the best path down the Map for the desired field. --- .../preprocessing/FrequencyEncoding.java | 3 +- .../preprocessing/OneHotEncoding.java | 3 +- .../preprocessing/TargetMeanEncoding.java | 3 +- .../ml/inference/trainedmodel/tree/Tree.java | 5 +- .../xpack/core/ml/utils/MapHelper.java | 133 ++++++++++++ .../preprocessing/FrequencyEncodingTests.java | 18 ++ .../preprocessing/OneHotEncodingTests.java | 15 ++ .../TargetMeanEncodingTests.java | 20 ++ .../trainedmodel/ensemble/EnsembleTests.java | 57 ++++++ .../trainedmodel/tree/TreeTests.java | 52 +++++ .../xpack/core/ml/utils/MapHelperTests.java | 193 ++++++++++++++++++ .../inference/ingest/InferenceProcessor.java | 3 +- .../inference/loadingservice/LocalModel.java | 4 +- .../ingest/InferenceProcessorTests.java | 44 +++- .../loadingservice/LocalModelTests.java | 14 +- .../integration/ModelInferenceActionIT.java | 48 +++-- 16 files changed, 582 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/MapHelper.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/MapHelperTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/FrequencyEncoding.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/FrequencyEncoding.java index ed693460edcc7..e9606d53ae27d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/FrequencyEncoding.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/FrequencyEncoding.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; +import org.elasticsearch.xpack.core.ml.utils.MapHelper; import java.io.IOException; import java.util.Collections; @@ -103,7 +104,7 @@ public String getName() { @Override public void process(Map fields) { - Object value = fields.get(field); + Object value = MapHelper.dig(field, fields); if (value == null) { return; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/OneHotEncoding.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/OneHotEncoding.java index a4924a277c0ad..9bb2537b61ed3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/OneHotEncoding.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/OneHotEncoding.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; +import org.elasticsearch.xpack.core.ml.utils.MapHelper; import java.io.IOException; import java.util.Collections; @@ -86,7 +87,7 @@ public String getName() { @Override public void process(Map fields) { - Object value = fields.get(field); + Object value = MapHelper.dig(field, fields); if (value == null) { return; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/TargetMeanEncoding.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/TargetMeanEncoding.java index 8276fc2c8fefb..19c3cadbbef95 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/TargetMeanEncoding.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/TargetMeanEncoding.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; +import org.elasticsearch.xpack.core.ml.utils.MapHelper; import java.io.IOException; import java.util.Collections; @@ -114,7 +115,7 @@ public String getName() { @Override public void process(Map fields) { - Object value = fields.get(field); + Object value = MapHelper.dig(field, fields); if (value == null) { return; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/Tree.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/Tree.java index b137c8f28d58e..831838e0f7df2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/Tree.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/Tree.java @@ -28,6 +28,7 @@ import org.elasticsearch.xpack.core.ml.inference.trainedmodel.StrictlyParsedTrainedModel; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.TargetType; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; +import org.elasticsearch.xpack.core.ml.utils.MapHelper; import java.io.IOException; import java.util.ArrayDeque; @@ -129,7 +130,9 @@ public InferenceResults infer(Map fields, InferenceConfig config "Cannot infer using configuration for [{}] when model target_type is [{}]", config.getName(), targetType.toString()); } - List features = featureNames.stream().map(f -> InferenceHelpers.toDouble(fields.get(f))).collect(Collectors.toList()); + List features = featureNames.stream() + .map(f -> InferenceHelpers.toDouble(MapHelper.dig(f, fields))) + .collect(Collectors.toList()); return infer(features, config); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/MapHelper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/MapHelper.java new file mode 100644 index 0000000000000..dcd74af158173 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/MapHelper.java @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.utils; + +import org.elasticsearch.common.Nullable; + +import java.util.Arrays; +import java.util.Map; +import java.util.Stack; + +public final class MapHelper { + + private MapHelper() {} + + /** + * This eagerly digs (depth first search, longer keys first) through the map by tokenizing the provided path on '.'. + * + * It is possible for ES _source docs to have "mixed" path formats. So, we should search all potential paths + * given the current knowledge of the map. + * + * Examples: + * + * The following maps would return `2` given the path "a.b.c.d" + * + * { + * "a.b.c.d" : 2 + * } + * { + * "a" :{"b": {"c": {"d" : 2}}} + * } + * { + * "a" :{"b.c": {"d" : 2}}} + * } + * { + * "a" :{"b": {"c": {"d" : 2}}}, + * "a.b" :{"c": {"d" : 5}} // we choose the first one found, we go down longer keys first + * } + * { + * "a" :{"b": {"c": {"NOT_d" : 2, "d": 2}}} + * } + * + * Conceptual "Worse case" 5 potential paths explored for "a.b.c.d" until 2 is finally returned + * { + * "a.b.c": {"not_d": 2}, + * "a.b": {"c": {"not_d": 2}}, + * "a": {"b.c": {"not_d": 2}}, + * "a": {"b" :{ "c.not_d": 2}}, + * "a" :{"b": {"c": {"not_d" : 2}}}, + * "a" :{"b": {"c": {"d" : 2}}}, + * } + * + * We don't exhaustively create all potential paths. + * If we did, this would result in 2^n-1 total possible paths, where {@code n = path.split("\\.").length}. + * + * Instead we lazily create potential paths once we know that they are possibilities. + * + * @param path Dot delimited path containing the field desired + * @param map The {@link Map} map to dig + * @return The found object. Returns {@code null} if not found + */ + @Nullable + public static Object dig(String path, Map map) { + // short cut before search + if (map.keySet().contains(path)) { + return map.get(path); + } + String[] fields = path.split("\\."); + if (Arrays.stream(fields).anyMatch(String::isEmpty)) { + throw new IllegalArgumentException("Empty path detected. Invalid field name"); + } + Stack pathStack = new Stack<>(); + pathStack.push(new PotentialPath(map, 0)); + return explore(fields, pathStack); + } + + @SuppressWarnings("unchecked") + private static Object explore(String[] path, Stack pathStack) { + while (pathStack.empty() == false) { + PotentialPath potentialPath = pathStack.pop(); + int endPos = potentialPath.pathPosition + 1; + int startPos = potentialPath.pathPosition; + Map map = potentialPath.map; + String candidateKey = null; + while(endPos <= path.length) { + candidateKey = mergePath(path, startPos, endPos); + Object next = map.get(candidateKey); + if (endPos == path.length && next != null) { // exit early, we reached the full path and found something + return next; + } + if (next instanceof Map) { // we found another map, continue exploring down this path + pathStack.push(new PotentialPath((Map)next, endPos)); + } + endPos++; + } + if (candidateKey != null && map.containsKey(candidateKey)) { //exit early + return map.get(candidateKey); + } + } + + return null; + } + + private static String mergePath(String[] path, int start, int end) { + if (start + 1 == end) { // early exit, no need to create sb + return path[start]; + } + + StringBuilder sb = new StringBuilder(); + for (int i = start; i < end - 1; i++) { + sb.append(path[i]); + sb.append("."); + } + sb.append(path[end - 1]); + return sb.toString(); + } + + private static class PotentialPath { + + // Pointer to where to start exploring + private final Map map; + // Where in the requested path are we + private final int pathPosition; + + private PotentialPath(Map map, int pathPosition) { + this.map = map; + this.pathPosition = pathPosition; + } + + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/FrequencyEncodingTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/FrequencyEncodingTests.java index 4c0497fa409f9..5e296c8f11f81 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/FrequencyEncodingTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/FrequencyEncodingTests.java @@ -65,4 +65,22 @@ public void testProcessWithFieldPresent() { testProcess(encoding, fieldValues, matchers); } + public void testProcessWithNestedField() { + String field = "categorical.child"; + List values = Arrays.asList("foo", "bar", "foobar", "baz", "farequote", 1.5); + Map valueMap = values.stream().collect(Collectors.toMap(Object::toString, + v -> randomDoubleBetween(0.0, 1.0, false))); + String encodedFeatureName = "encoded"; + FrequencyEncoding encoding = new FrequencyEncoding(field, encodedFeatureName, valueMap); + + Map fieldValues = new HashMap<>() {{ + put("categorical", new HashMap<>(){{ + put("child", "farequote"); + }}); + }}; + + encoding.process(fieldValues); + assertThat(fieldValues.get("encoded"), equalTo(valueMap.get("farequote"))); + } + } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/OneHotEncodingTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/OneHotEncodingTests.java index 8b35b77b5a69c..c065d652fcb1a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/OneHotEncodingTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/OneHotEncodingTests.java @@ -67,4 +67,19 @@ public void testProcessWithFieldPresent() { testProcess(encoding, fieldValues, matchers); } + public void testProcessWithNestedField() { + String field = "categorical.child"; + List values = Arrays.asList("foo", "bar", "foobar", "baz", "farequote", 1.5); + Map valueMap = values.stream().collect(Collectors.toMap(Object::toString, v -> "Column_" + v.toString())); + OneHotEncoding encoding = new OneHotEncoding(field, valueMap); + Map fieldValues = new HashMap<>() {{ + put("categorical", new HashMap<>(){{ + put("child", "farequote"); + }}); + }}; + + encoding.process(fieldValues); + assertThat(fieldValues.get("Column_farequote"), equalTo(1)); + } + } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/TargetMeanEncodingTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/TargetMeanEncodingTests.java index e2aaf1e1256c6..8143b5118ed70 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/TargetMeanEncodingTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/preprocessing/TargetMeanEncodingTests.java @@ -68,4 +68,24 @@ public void testProcessWithFieldPresent() { testProcess(encoding, fieldValues, matchers); } + public void testProcessWithNestedField() { + String field = "categorical.child"; + List values = Arrays.asList("foo", "bar", "foobar", "baz", "farequote", 1.5); + Map valueMap = values.stream().collect(Collectors.toMap(Object::toString, + v -> randomDoubleBetween(0.0, 1.0, false))); + String encodedFeatureName = "encoded"; + Double defaultvalue = randomDouble(); + TargetMeanEncoding encoding = new TargetMeanEncoding(field, encodedFeatureName, valueMap, defaultvalue); + + Map fieldValues = new HashMap<>() {{ + put("categorical", new HashMap<>(){{ + put("child", "farequote"); + }}); + }}; + + encoding.process(fieldValues); + + assertThat(fieldValues.get("encoded"), equalTo(valueMap.get("farequote"))); + } + } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/EnsembleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/EnsembleTests.java index 13a401117b479..46373dae834c9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/EnsembleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ensemble/EnsembleTests.java @@ -445,6 +445,63 @@ public void testRegressionInference() { closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); } + public void testInferNestedFields() { + List featureNames = Arrays.asList("foo.baz", "bar.biz"); + Tree tree1 = Tree.builder() + .setFeatureNames(featureNames) + .setRoot(TreeNode.builder(0) + .setLeftChild(1) + .setRightChild(2) + .setSplitFeature(0) + .setThreshold(0.5)) + .addNode(TreeNode.builder(1).setLeafValue(0.3)) + .addNode(TreeNode.builder(2) + .setThreshold(0.8) + .setSplitFeature(1) + .setLeftChild(3) + .setRightChild(4)) + .addNode(TreeNode.builder(3).setLeafValue(0.1)) + .addNode(TreeNode.builder(4).setLeafValue(0.2)).build(); + Tree tree2 = Tree.builder() + .setFeatureNames(featureNames) + .setRoot(TreeNode.builder(0) + .setLeftChild(1) + .setRightChild(2) + .setSplitFeature(0) + .setThreshold(0.5)) + .addNode(TreeNode.builder(1).setLeafValue(1.5)) + .addNode(TreeNode.builder(2).setLeafValue(0.9)) + .build(); + Ensemble ensemble = Ensemble.builder() + .setTargetType(TargetType.REGRESSION) + .setFeatureNames(featureNames) + .setTrainedModels(Arrays.asList(tree1, tree2)) + .setOutputAggregator(new WeightedSum(new double[]{0.5, 0.5})) + .build(); + + Map featureMap = new HashMap<>() {{ + put("foo", new HashMap<>(){{ + put("baz", 0.4); + }}); + put("bar", new HashMap<>(){{ + put("biz", 0.0); + }}); + }}; + assertThat(0.9, + closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); + + featureMap = new HashMap<>() {{ + put("foo", new HashMap<>(){{ + put("baz", 2.0); + }}); + put("bar", new HashMap<>(){{ + put("biz", 0.7); + }}); + }}; + assertThat(0.5, + closeTo(((SingleValueInferenceResults)ensemble.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); + } + public void testOperationsEstimations() { Tree tree1 = TreeTests.buildRandomTree(Arrays.asList("foo", "bar"), 2); Tree tree2 = TreeTests.buildRandomTree(Arrays.asList("foo", "bar", "baz"), 5); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/TreeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/TreeTests.java index 9d682896569bb..8c05c8d7b9d3a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/TreeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/tree/TreeTests.java @@ -169,6 +169,58 @@ public void testInfer() { closeTo(((SingleValueInferenceResults)tree.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); } + public void testInferNestedFields() { + // Build a tree with 2 nodes and 3 leaves using 2 features + // The leaves have unique values 0.1, 0.2, 0.3 + Tree.Builder builder = Tree.builder().setTargetType(TargetType.REGRESSION); + TreeNode.Builder rootNode = builder.addJunction(0, 0, true, 0.5); + builder.addLeaf(rootNode.getRightChild(), 0.3); + TreeNode.Builder leftChildNode = builder.addJunction(rootNode.getLeftChild(), 1, true, 0.8); + builder.addLeaf(leftChildNode.getLeftChild(), 0.1); + builder.addLeaf(leftChildNode.getRightChild(), 0.2); + + List featureNames = Arrays.asList("foo.baz", "bar.biz"); + Tree tree = builder.setFeatureNames(featureNames).build(); + + // This feature vector should hit the right child of the root node + Map featureMap = new HashMap<>() {{ + put("foo", new HashMap<>(){{ + put("baz", 0.6); + }}); + put("bar", new HashMap<>(){{ + put("biz", 0.0); + }}); + }}; + assertThat(0.3, + closeTo(((SingleValueInferenceResults)tree.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); + + // This should hit the left child of the left child of the root node + // i.e. it takes the path left, left + featureMap = new HashMap<>() {{ + put("foo", new HashMap<>(){{ + put("baz", 0.3); + }}); + put("bar", new HashMap<>(){{ + put("biz", 0.7); + }}); + }}; + assertThat(0.1, + closeTo(((SingleValueInferenceResults)tree.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); + + // This should hit the right child of the left child of the root node + // i.e. it takes the path left, right + featureMap = new HashMap<>() {{ + put("foo", new HashMap<>(){{ + put("baz", 0.3); + }}); + put("bar", new HashMap<>(){{ + put("biz", 0.9); + }}); + }}; + assertThat(0.2, + closeTo(((SingleValueInferenceResults)tree.infer(featureMap, RegressionConfig.EMPTY_PARAMS)).value(), 0.00001)); + } + public void testTreeClassificationProbability() { // Build a tree with 2 nodes and 3 leaves using 2 features // The leaves have unique values 0.1, 0.2, 0.3 diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/MapHelperTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/MapHelperTests.java new file mode 100644 index 0000000000000..c3a7eeff76fe7 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/MapHelperTests.java @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.utils; + +import org.elasticsearch.test.ESTestCase; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +public class MapHelperTests extends ESTestCase { + + public void testAbsolutePathStringAsKey() { + String path = "a.b.c.d"; + Map map = Collections.singletonMap(path, 2); + assertThat(MapHelper.dig(path, map), equalTo(2)); + assertThat(MapHelper.dig(path, Collections.emptyMap()), is(nullValue())); + } + + public void testSimplePath() { + String path = "a.b.c.d"; + Map map = Collections.singletonMap("a", + Collections.singletonMap("b", + Collections.singletonMap("c", + Collections.singletonMap("d", 2)))); + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = Collections.singletonMap("a", + Collections.singletonMap("b", + Collections.singletonMap("e", // Not part of path + Collections.singletonMap("d", 2)))); + assertThat(MapHelper.dig(path, map), is(nullValue())); + } + + public void testSimplePathReturningMap() { + String path = "a.b.c"; + Map map = Collections.singletonMap("a", + Collections.singletonMap("b", + Collections.singletonMap("c", + Collections.singletonMap("d", 2)))); + assertThat(MapHelper.dig(path, map), equalTo(Collections.singletonMap("d", 2))); + } + + public void testSimpleMixedPath() { + String path = "a.b.c.d"; + Map map = Collections.singletonMap("a", + Collections.singletonMap("b.c", + Collections.singletonMap("d", 2))); + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = Collections.singletonMap("a.b", + Collections.singletonMap("c", + Collections.singletonMap("d", 2))); + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = Collections.singletonMap("a.b.c", + Collections.singletonMap("d", 2)); + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = Collections.singletonMap("a", + Collections.singletonMap("b", + Collections.singletonMap("c.d", 2))); + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = Collections.singletonMap("a", + Collections.singletonMap("b.c.d", 2)); + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = Collections.singletonMap("a.b", + Collections.singletonMap("c.d", 2)); + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = Collections.singletonMap("a", + Collections.singletonMap("b.foo", + Collections.singletonMap("d", 2))); + assertThat(MapHelper.dig(path, map), is(nullValue())); + + map = Collections.singletonMap("a", + Collections.singletonMap("b.c", + Collections.singletonMap("foo", 2))); + assertThat(MapHelper.dig(path, map), is(nullValue())); + + map = Collections.singletonMap("x", + Collections.singletonMap("b.c", + Collections.singletonMap("d", 2))); + assertThat(MapHelper.dig(path, map), is(nullValue())); + } + + public void testSimpleMixedPathReturningMap() { + String path = "a.b.c"; + Map map = Collections.singletonMap("a", + Collections.singletonMap("b.c", + Collections.singletonMap("d", 2))); + assertThat(MapHelper.dig(path, map), equalTo(Collections.singletonMap("d", 2))); + + map = Collections.singletonMap("a", + Collections.singletonMap("b.foo", + Collections.singletonMap("d", 2))); + assertThat(MapHelper.dig(path, map), is(nullValue())); + + map = Collections.singletonMap("a", + Collections.singletonMap("b.not_c", + Collections.singletonMap("foo", 2))); + assertThat(MapHelper.dig(path, map), is(nullValue())); + + map = Collections.singletonMap("x", + Collections.singletonMap("b.c", + Collections.singletonMap("d", 2))); + assertThat(MapHelper.dig(path, map), is(nullValue())); + } + + public void testMultiplePotentialPaths() { + String path = "a.b.c.d"; + Map map = new LinkedHashMap<>() {{ + put("a", Collections.singletonMap("b", + Collections.singletonMap("c", + Collections.singletonMap("not_d", 5)))); + put("a.b", Collections.singletonMap("c", Collections.singletonMap("d", 2))); + }}; + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = new LinkedHashMap<>() {{ + put("a", Collections.singletonMap("b", + Collections.singletonMap("c", + Collections.singletonMap("d", 2)))); + put("a.b", Collections.singletonMap("c", Collections.singletonMap("not_d", 5))); + }}; + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = new LinkedHashMap<>() {{ + put("a", Collections.singletonMap("b", + new HashMap<>() {{ + put("c", Collections.singletonMap("not_d", 5)); + put("c.d", 2); + }})); + }}; + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = new LinkedHashMap<>() {{ + put("a", Collections.singletonMap("b", + new HashMap<>() {{ + put("c", Collections.singletonMap("d", 2)); + put("c.not_d", 5); + }})); + }}; + assertThat(MapHelper.dig(path, map), equalTo(2)); + + map = new LinkedHashMap<>() {{ + put("a", Collections.singletonMap("b", + Collections.singletonMap("c", + Collections.singletonMap("not_d", 5)))); + put("a.b", Collections.singletonMap("c", Collections.singletonMap("not_d", 2))); + }}; + + assertThat(MapHelper.dig(path, map), is(nullValue())); + } + + public void testMultiplePotentialPathsReturningMap() { + String path = "a.b.c"; + Map map = new LinkedHashMap<>() {{ + put("a", Collections.singletonMap("b", + Collections.singletonMap("c", + Collections.singletonMap("d", 2)))); + put("a.b", Collections.singletonMap("not_c", Collections.singletonMap("d", 2))); + }}; + assertThat(MapHelper.dig(path, map), equalTo(Collections.singletonMap("d", 2))); + + map = new LinkedHashMap<>() {{ + put("a", Collections.singletonMap("b", + Collections.singletonMap("not_c", + Collections.singletonMap("d", 2)))); + put("a.b", Collections.singletonMap("c", Collections.singletonMap("d", 2))); + }}; + assertThat(MapHelper.dig(path, map), equalTo(Collections.singletonMap("d", 2))); + + map = new LinkedHashMap<>() {{ + put("a", Collections.singletonMap("b", + Collections.singletonMap("not_c", + Collections.singletonMap("d", 2)))); + put("a.b", Collections.singletonMap("not_c", Collections.singletonMap("d", 2))); + }}; + assertThat(MapHelper.dig(path, map), is(nullValue())); + } + +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java index 19c0054b522bf..18ac57d1ae818 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java @@ -35,6 +35,7 @@ import org.elasticsearch.xpack.core.ml.inference.trainedmodel.RegressionConfig; import org.elasticsearch.xpack.core.ml.job.messages.Messages; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; +import org.elasticsearch.xpack.core.ml.utils.MapHelper; import org.elasticsearch.xpack.ml.notifications.InferenceAuditor; import java.util.Arrays; @@ -128,7 +129,7 @@ InternalInferModelAction.Request buildRequest(IngestDocument ingestDocument) { Map fields = new HashMap<>(ingestDocument.getSourceAndMetadata()); if (fieldMapping != null) { fieldMapping.forEach((src, dest) -> { - Object srcValue = fields.remove(src); + Object srcValue = MapHelper.dig(src, fields); if (srcValue != null) { fields.put(dest, srcValue); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModel.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModel.java index 4e62c69336b6a..8a233d534a76f 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModel.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModel.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.ml.inference.loadingservice; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.xpack.core.ml.inference.TrainedModelDefinition; import org.elasticsearch.xpack.core.ml.inference.TrainedModelInput; import org.elasticsearch.xpack.core.ml.inference.results.WarningInferenceResults; @@ -16,6 +15,7 @@ import org.elasticsearch.xpack.core.ml.inference.results.ClassificationInferenceResults; import org.elasticsearch.xpack.core.ml.inference.results.InferenceResults; import org.elasticsearch.xpack.core.ml.inference.results.RegressionInferenceResults; +import org.elasticsearch.xpack.core.ml.utils.MapHelper; import java.util.HashSet; import java.util.Map; @@ -61,7 +61,7 @@ public String getResultsType() { @Override public void infer(Map fields, InferenceConfig config, ActionListener listener) { try { - if (Sets.haveEmptyIntersection(fieldNames, fields.keySet())) { + if (fieldNames.stream().allMatch(f -> MapHelper.dig(f, fields) == null)) { listener.onResponse(new WarningInferenceResults(Messages.getMessage(INFERENCE_WARNING_ALL_FIELDS_MISSING, modelId))); return; } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java index 014c9fad11203..57389b3660241 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessorTests.java @@ -181,7 +181,7 @@ public void testGenerateWithMapping() { String modelId = "model"; Integer topNClasses = randomBoolean() ? null : randomIntBetween(1, 10); - Map fieldMapping = new HashMap<>(3) {{ + Map fieldMapping = new HashMap<>(5) {{ put("value1", "new_value1"); put("value2", "new_value2"); put("categorical", "new_categorical"); @@ -195,7 +195,7 @@ public void testGenerateWithMapping() { new ClassificationConfig(topNClasses, null, null), fieldMapping); - Map source = new HashMap<>(3){{ + Map source = new HashMap<>(5){{ put("value1", 1); put("categorical", "foo"); put("un_touched", "bar"); @@ -203,8 +203,46 @@ public void testGenerateWithMapping() { Map ingestMetadata = new HashMap<>(); IngestDocument document = new IngestDocument(source, ingestMetadata); - Map expectedMap = new HashMap<>(2) {{ + Map expectedMap = new HashMap<>(7) {{ put("new_value1", 1); + put("value1", 1); + put("categorical", "foo"); + put("new_categorical", "foo"); + put("un_touched", "bar"); + }}; + assertThat(processor.buildRequest(document).getObjectsToInfer().get(0), equalTo(expectedMap)); + } + + public void testGenerateWithMappingNestedFields() { + String modelId = "model"; + Integer topNClasses = randomBoolean() ? null : randomIntBetween(1, 10); + + Map fieldMapping = new HashMap<>(5) {{ + put("value1.foo", "new_value1"); + put("value2", "new_value2"); + put("categorical.bar", "new_categorical"); + }}; + + InferenceProcessor processor = new InferenceProcessor(client, + auditor, + "my_processor", + "my_field", + modelId, + new ClassificationConfig(topNClasses, null, null), + fieldMapping); + + Map source = new HashMap<>(5){{ + put("value1", Collections.singletonMap("foo", 1)); + put("categorical.bar", "foo"); + put("un_touched", "bar"); + }}; + Map ingestMetadata = new HashMap<>(); + IngestDocument document = new IngestDocument(source, ingestMetadata); + + Map expectedMap = new HashMap<>(7) {{ + put("new_value1", 1); + put("value1", Collections.singletonMap("foo", 1)); + put("categorical.bar", "foo"); put("new_categorical", "foo"); put("un_touched", "bar"); }}; diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java index fb82c4ddfaaf5..faa1717cc4fde 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/loadingservice/LocalModelTests.java @@ -41,7 +41,7 @@ public class LocalModelTests extends ESTestCase { public void testClassificationInfer() throws Exception { String modelId = "classification_model"; - List inputFields = Arrays.asList("foo", "bar", "categorical"); + List inputFields = Arrays.asList("field.foo", "field.bar", "categorical"); TrainedModelDefinition definition = new TrainedModelDefinition.Builder() .setPreProcessors(Arrays.asList(new OneHotEncoding("categorical", oneHotMap()))) .setTrainedModel(buildClassification(false)) @@ -49,8 +49,8 @@ public void testClassificationInfer() throws Exception { Model model = new LocalModel(modelId, definition, new TrainedModelInput(inputFields)); Map fields = new HashMap<>() {{ - put("foo", 1.0); - put("bar", 0.5); + put("field.foo", 1.0); + put("field.bar", 0.5); put("categorical", "dog"); }}; @@ -93,8 +93,8 @@ public void testRegression() throws Exception { Model model = new LocalModel("regression_model", trainedModelDefinition, new TrainedModelInput(inputFields)); Map fields = new HashMap<>() {{ - put("foo", 1.0); - put("bar", 0.5); + put("field.foo", 1.0); + put("field.bar", 0.5); put("categorical", "dog"); }}; @@ -147,7 +147,7 @@ private static Map oneHotMap() { } public static TrainedModel buildClassification(boolean includeLabels) { - List featureNames = Arrays.asList("foo", "bar", "animal_cat", "animal_dog"); + List featureNames = Arrays.asList("field.foo", "field.bar", "animal_cat", "animal_dog"); Tree tree1 = Tree.builder() .setFeatureNames(featureNames) .setRoot(TreeNode.builder(0) @@ -193,7 +193,7 @@ public static TrainedModel buildClassification(boolean includeLabels) { } public static TrainedModel buildRegression() { - List featureNames = Arrays.asList("foo", "bar", "animal_cat", "animal_dog"); + List featureNames = Arrays.asList("field.foo", "field.bar", "animal_cat", "animal_dog"); Tree tree1 = Tree.builder() .setFeatureNames(featureNames) .setRoot(TreeNode.builder(0) diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java index 6f48ce7d3e745..86d5f278c42a3 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/ModelInferenceActionIT.java @@ -66,9 +66,9 @@ public void testInferModels() throws Exception { oneHotEncoding.put("cat", "animal_cat"); oneHotEncoding.put("dog", "animal_dog"); TrainedModelConfig config1 = buildTrainedModelConfigBuilder(modelId2) - .setInput(new TrainedModelInput(Arrays.asList("foo", "bar", "categorical"))) + .setInput(new TrainedModelInput(Arrays.asList("field.foo", "field.bar", "other.categorical"))) .setParsedDefinition(new TrainedModelDefinition.Builder() - .setPreProcessors(Arrays.asList(new OneHotEncoding("categorical", oneHotEncoding))) + .setPreProcessors(Arrays.asList(new OneHotEncoding("other.categorical", oneHotEncoding))) .setTrainedModel(buildClassification(true))) .setVersion(Version.CURRENT) .setLicenseLevel(License.OperationMode.PLATINUM.description()) @@ -77,9 +77,9 @@ public void testInferModels() throws Exception { .setEstimatedHeapMemory(0) .build(); TrainedModelConfig config2 = buildTrainedModelConfigBuilder(modelId1) - .setInput(new TrainedModelInput(Arrays.asList("foo", "bar", "categorical"))) + .setInput(new TrainedModelInput(Arrays.asList("field.foo", "field.bar", "other.categorical"))) .setParsedDefinition(new TrainedModelDefinition.Builder() - .setPreProcessors(Arrays.asList(new OneHotEncoding("categorical", oneHotEncoding))) + .setPreProcessors(Arrays.asList(new OneHotEncoding("other.categorical", oneHotEncoding))) .setTrainedModel(buildRegression())) .setVersion(Version.CURRENT) .setEstimatedOperations(0) @@ -99,26 +99,42 @@ public void testInferModels() throws Exception { List> toInfer = new ArrayList<>(); toInfer.add(new HashMap<>() {{ - put("foo", 1.0); - put("bar", 0.5); - put("categorical", "dog"); + put("field", new HashMap<>(){{ + put("foo", 1.0); + put("bar", 0.5); + }}); + put("other", new HashMap<>(){{ + put("categorical", "dog"); + }}); }}); toInfer.add(new HashMap<>() {{ - put("foo", 0.9); - put("bar", 1.5); - put("categorical", "cat"); + put("field", new HashMap<>(){{ + put("foo", 0.9); + put("bar", 1.5); + }}); + put("other", new HashMap<>(){{ + put("categorical", "cat"); + }}); }}); List> toInfer2 = new ArrayList<>(); toInfer2.add(new HashMap<>() {{ - put("foo", 0.0); - put("bar", 0.01); - put("categorical", "dog"); + put("field", new HashMap<>(){{ + put("foo", 0.0); + put("bar", 0.01); + }}); + put("other", new HashMap<>(){{ + put("categorical", "dog"); + }}); }}); toInfer2.add(new HashMap<>() {{ - put("foo", 1.0); - put("bar", 0.0); - put("categorical", "cat"); + put("field", new HashMap<>(){{ + put("foo", 1.0); + put("bar", 0.0); + }}); + put("other", new HashMap<>(){{ + put("categorical", "cat"); + }}); }}); // Test regression From a53ce5259cd205712542a632337645d281455c27 Mon Sep 17 00:00:00 2001 From: lcawl Date: Wed, 18 Dec 2019 11:46:00 -0800 Subject: [PATCH 259/686] [DOCS] Fixes security links --- docs/reference/ml/anomaly-detection/stopping-ml.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ml/anomaly-detection/stopping-ml.asciidoc b/docs/reference/ml/anomaly-detection/stopping-ml.asciidoc index e91776e287aec..9902ef59857f8 100644 --- a/docs/reference/ml/anomaly-detection/stopping-ml.asciidoc +++ b/docs/reference/ml/anomaly-detection/stopping-ml.asciidoc @@ -30,7 +30,7 @@ POST _ml/datafeeds/feed1/_stop // TEST[skip:setup:server_metrics_startdf] NOTE: You must have `manage_ml`, or `manage` cluster privileges to stop {dfeeds}. -For more information, see <>. +For more information, see {ref}/security-privileges.html[Security privileges] A {dfeed} can be started and stopped multiple times throughout its lifecycle. @@ -69,7 +69,7 @@ POST _ml/anomaly_detectors/job1/_close // TEST[skip:setup:server_metrics_openjob] NOTE: You must have `manage_ml`, or `manage` cluster privileges to stop {dfeeds}. -For more information, see <>. +For more information, see {ref}/security-privileges.html[Security privileges] {anomaly-jobs-cap} can be opened and closed multiple times throughout their lifecycle. From 603ebef7a723275b700adbc82745e1841de447e8 Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Wed, 18 Dec 2019 13:29:17 -0700 Subject: [PATCH 260/686] [TEST] Exclude name on ScriptContextInfo mutate (#50332) ScriptContextInfoSerializingTests:testEqualsAndHashcode was failing because the mutation was generating the same name. Fixes: #50331 --- .../storedscripts/ScriptContextInfoSerializingTests.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptContextInfoSerializingTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptContextInfoSerializingTests.java index 3a9fb4d3baa39..588a39489b737 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptContextInfoSerializingTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptContextInfoSerializingTests.java @@ -56,7 +56,10 @@ protected ScriptContextInfo mutateInstance(ScriptContextInfo instance) throws IO } private static ScriptContextInfo mutate(ScriptContextInfo instance, Set names) { - if (names == null) { names = new HashSet<>(); } + if (names == null) { + names = new HashSet<>(); + names.add(instance.name); + } switch (randomIntBetween(0, 2)) { case 0: return new ScriptContextInfo( From b6211cdbfdc8b7bd480860195be7caefb083d25c Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Wed, 18 Dec 2019 16:57:38 -0500 Subject: [PATCH 261/686] [DOCS] Document `thread_pool` node stats (#50330) --- docs/reference/cluster/nodes-stats.asciidoc | 41 +++++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/docs/reference/cluster/nodes-stats.asciidoc b/docs/reference/cluster/nodes-stats.asciidoc index d607c6430d789..af3ad0deee442 100644 --- a/docs/reference/cluster/nodes-stats.asciidoc +++ b/docs/reference/cluster/nodes-stats.asciidoc @@ -895,6 +895,33 @@ Total number of buffer pool classes loaded since the JVM started. (integer) Total number of buffer pool classes unloaded since the JVM started. +[[cluster-nodes-stats-api-response-body-threadpool]] +===== `thread_pool` section + +`thread_pool..threads`:: +(integer) +Number of threads in the thread pool. + +`thread_pool..queue`:: +(integer) +Number of tasks in queue for the thread pool. + +`thread_pool..active`:: +(integer) +Number of active threads in the thread pool. + +`thread_pool..rejected`:: +(integer) +Number of tasks rejected by the thread pool executor. + +`thread_pool..largest`:: +(integer) +Highest number of active threads in the thread pool. + +`thread_pool..completed`:: +(integer) +Number of tasks completed by the thread pool executor. + [[cluster-nodes-stats-api-response-body-ingest]] ===== `ingest` section @@ -915,31 +942,31 @@ Total number of buffer pool classes unloaded since the JVM started. (integer) Total number of failed ingest operations during the lifetime of this node. -`ingest.pipelines..count`:: +`ingest.pipelines..count`:: (integer) Number of documents preprocessed by the ingest pipeline. -`ingest.pipelines..time_in_millis`:: +`ingest.pipelines..time_in_millis`:: (integer) Total time spent preprocessing documents in the ingest pipeline. -`ingest.pipelines..failed`:: +`ingest.pipelines..failed`:: (integer) Total number of failed operations for the ingest pipeline. -`ingest.pipelines...count`:: +`ingest.pipelines...count`:: (integer) Number of documents transformed by the processor. -`ingest.pipelines...time_in_millis`:: +`ingest.pipelines...time_in_millis`:: (integer) Time spent by the processor transforming documents. -`ingest.pipelines...current`:: +`ingest.pipelines...current`:: (integer) Number of documents currently being transformed by the processor. -`ingest.pipelines...failed`:: +`ingest.pipelines...failed`:: (integer) Number of failed operations for the processor. From 49c05ee14cb97b2834bfc232b2dc1077b555cba4 Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Wed, 18 Dec 2019 15:03:53 -0700 Subject: [PATCH 262/686] [TEST] BWC re-enable after backport of #50106 (#50336) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 6f786b5c3beea..df1641dcc9efe 100644 --- a/build.gradle +++ b/build.gradle @@ -205,8 +205,8 @@ task verifyVersions { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = false -final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/50106" /* place a PR link here when committing bwc changes */ +boolean bwc_tests_enabled = true +final String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */ if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") From 16dcea199c9daa829d44112f5fa6175464104712 Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Wed, 18 Dec 2019 16:09:59 -0700 Subject: [PATCH 263/686] Add ILM histore store index (#50287) * Add ILM histore store index This commit adds an ILM history store that tracks the lifecycle execution state as an index progresses through its ILM policy. ILM history documents store output similar to what the ILM explain API returns. An example document with ALL fields (not all documents will have all fields) would look like: ```json { "@timestamp": 1203012389, "policy": "my-ilm-policy", "index": "index-2019.1.1-000023", "index_age":123120, "success": true, "state": { "phase": "warm", "action": "allocate", "step": "ERROR", "failed_step": "update-settings", "is_auto-retryable_error": true, "creation_date": 12389012039, "phase_time": 12908389120, "action_time": 1283901209, "step_time": 123904107140, "phase_definition": "{\"policy\":\"ilm-history-ilm-policy\",\"phase_definition\":{\"min_age\":\"0ms\",\"actions\":{\"rollover\":{\"max_size\":\"50gb\",\"max_age\":\"30d\"}}},\"version\":1,\"modified_date_in_millis\":1576517253463}", "step_info": "{... etc step info here as json ...}" }, "error_details": "java.lang.RuntimeException: etc\n\tcaused by:etc etc etc full stacktrace" } ``` These documents go into the `ilm-history-1-00000N` index to provide an audit trail of the operations ILM has performed. This history storage is enabled by default but can be disabled by setting `index.lifecycle.history_index_enabled` to `false.` Resolves #49180 --- client/rest-high-level/build.gradle | 1 + docs/build.gradle | 1 + docs/plugins/analysis-icu.asciidoc | 2 +- .../reference/indices/rollover-index.asciidoc | 2 +- docs/reference/settings/ilm-settings.asciidoc | 5 + .../test/rest/ESRestTestCase.java | 24 +- .../xpack/core/ilm/LifecycleSettings.java | 3 + .../resources/ilm-history-ilm-policy.json | 18 + .../core/src/main/resources/ilm-history.json | 83 ++++ x-pack/plugin/ilm/qa/multi-node/build.gradle | 2 + .../ilm/TimeSeriesLifecycleActionsIT.java | 172 +++++++- .../xpack/ilm/ExecuteStepsUpdateTask.java | 52 ++- .../xpack/ilm/IndexLifecycle.java | 14 +- .../xpack/ilm/IndexLifecycleRunner.java | 128 +++++- .../xpack/ilm/IndexLifecycleService.java | 8 +- .../xpack/ilm/MoveToErrorStepUpdateTask.java | 13 +- .../xpack/ilm/history/ILMHistoryItem.java | 115 +++++ .../xpack/ilm/history/ILMHistoryStore.java | 189 +++++++++ .../history/ILMHistoryTemplateRegistry.java | 80 ++++ .../IndexLifecycleInitialisationTests.java | 3 +- .../xpack/ilm/IndexLifecycleRunnerTests.java | 77 +++- .../xpack/ilm/IndexLifecycleServiceTests.java | 2 +- .../ilm/MoveToErrorStepUpdateTaskTests.java | 8 +- .../ilm/history/ILMHistoryItemTests.java | 92 ++++ .../ilm/history/ILMHistoryStoreTests.java | 398 ++++++++++++++++++ .../test/MonitoringIntegTestCase.java | 1 + .../AbstractWatcherIntegrationTestCase.java | 1 + 27 files changed, 1419 insertions(+), 75 deletions(-) create mode 100644 x-pack/plugin/core/src/main/resources/ilm-history-ilm-policy.json create mode 100644 x-pack/plugin/core/src/main/resources/ilm-history.json create mode 100644 x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItem.java create mode 100644 x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java create mode 100644 x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryTemplateRegistry.java create mode 100644 x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItemTests.java create mode 100644 x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStoreTests.java diff --git a/client/rest-high-level/build.gradle b/client/rest-high-level/build.gradle index d1f63d837fdce..1df697cbc5f6e 100644 --- a/client/rest-high-level/build.gradle +++ b/client/rest-high-level/build.gradle @@ -138,6 +138,7 @@ testClusters.all { setting 'xpack.security.authc.realms.pki.pki1.delegation.enabled', 'true' setting 'indices.lifecycle.poll_interval', '1000ms' + setting 'index.lifecycle.history_index_enabled', 'false' keystore 'xpack.security.transport.ssl.truststore.secure_password', 'testnode' extraConfigFile 'roles.yml', file('roles.yml') user username: System.getProperty('tests.rest.cluster.username', 'test_user'), diff --git a/docs/build.gradle b/docs/build.gradle index a39c7e0d9f3bd..5d9b8ae0a73e8 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -60,6 +60,7 @@ testClusters.integTest { extraConfigFile 'hunspell/en_US/en_US.dic', project(":server").file('src/test/resources/indices/analyze/conf_dir/hunspell/en_US/en_US.dic') // Whitelist reindexing from the local node so we can test it. setting 'reindex.remote.whitelist', '127.0.0.1:*' + setting 'index.lifecycle.history_index_enabled', 'false' // TODO: remove this once cname is prepended to transport.publish_address by default in 8.0 systemProperty 'es.transport.cname_in_publish_address', 'true' diff --git a/docs/plugins/analysis-icu.asciidoc b/docs/plugins/analysis-icu.asciidoc index 19a8625f60403..d0b95ae7f497b 100644 --- a/docs/plugins/analysis-icu.asciidoc +++ b/docs/plugins/analysis-icu.asciidoc @@ -368,7 +368,7 @@ PUT my_index } } -GET _search <3> +GET /my_index/_search <3> { "query": { "match": { diff --git a/docs/reference/indices/rollover-index.asciidoc b/docs/reference/indices/rollover-index.asciidoc index c4c7fb980bebb..b75961a52559e 100644 --- a/docs/reference/indices/rollover-index.asciidoc +++ b/docs/reference/indices/rollover-index.asciidoc @@ -428,7 +428,7 @@ PUT logs/_doc/2 <2> ////////////////////////// [source,console] -------------------------------------------------- -GET _alias +GET my_logs_index-000001,my_logs_index-000002/_alias -------------------------------------------------- // TEST[continued] ////////////////////////// diff --git a/docs/reference/settings/ilm-settings.asciidoc b/docs/reference/settings/ilm-settings.asciidoc index 8781581a6e57d..946848017bf7b 100644 --- a/docs/reference/settings/ilm-settings.asciidoc +++ b/docs/reference/settings/ilm-settings.asciidoc @@ -14,6 +14,11 @@ ILM REST API endpoints and functionality. Defaults to `true`. (<>) How often {ilm} checks for indices that meet policy criteria. Defaults to `10m`. +`index.lifecycle.history_index_enabled`:: +Whether ILM's history index is enabled. If enabled, ILM will record the +history of actions taken as part of ILM policies to the `ilm-history-*` +indices. Defaults to `true`. + ==== Index level settings These index-level {ilm-init} settings are typically configured through index templates. For more information, see <>. diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index 1eb52ac6de769..73ca1dde99ca8 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -75,6 +75,7 @@ import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -448,6 +449,13 @@ protected boolean preserveILMPoliciesUponCompletion() { return false; } + /** + * A set of ILM policies that should be preserved between runs. + */ + protected Set preserveILMPolicyIds() { + return Collections.singleton("ilm-history-ilm-policy"); + } + /** * Returns whether to preserve auto-follow patterns. Defaults to not * preserving them. Only runs at all if xpack is installed on the cluster @@ -545,7 +553,7 @@ private void wipeCluster() throws Exception { } if (hasXPack && false == preserveILMPoliciesUponCompletion()) { - deleteAllILMPolicies(); + deleteAllILMPolicies(preserveILMPolicyIds()); } if (hasXPack && false == preserveAutoFollowPatternsUponCompletion()) { @@ -680,7 +688,7 @@ private void waitForPendingRollupTasks() throws Exception { waitForPendingTasks(adminClient(), taskName -> taskName.startsWith("xpack/rollup/job") == false); } - private static void deleteAllILMPolicies() throws IOException { + private static void deleteAllILMPolicies(Set exclusions) throws IOException { Map policies; try { @@ -699,9 +707,15 @@ private static void deleteAllILMPolicies() throws IOException { return; } - for (String policyName : policies.keySet()) { - adminClient().performRequest(new Request("DELETE", "/_ilm/policy/" + policyName)); - } + policies.keySet().stream() + .filter(p -> exclusions.contains(p) == false) + .forEach(policyName -> { + try { + adminClient().performRequest(new Request("DELETE", "/_ilm/policy/" + policyName)); + } catch (IOException e) { + throw new RuntimeException("failed to delete policy: " + policyName, e); + } + }); } private static void deleteAllSLMPolicies() throws IOException { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleSettings.java index de037dc6f034e..b10f8defcdfc7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleSettings.java @@ -19,6 +19,7 @@ public class LifecycleSettings { public static final String LIFECYCLE_INDEXING_COMPLETE = "index.lifecycle.indexing_complete"; public static final String LIFECYCLE_ORIGINATION_DATE = "index.lifecycle.origination_date"; public static final String LIFECYCLE_PARSE_ORIGINATION_DATE = "index.lifecycle.parse_origination_date"; + public static final String LIFECYCLE_HISTORY_INDEX_ENABLED = "index.lifecycle.history_index_enabled"; public static final String SLM_HISTORY_INDEX_ENABLED = "slm.history_index_enabled"; public static final String SLM_RETENTION_SCHEDULE = "slm.retention_schedule"; @@ -35,6 +36,8 @@ public class LifecycleSettings { Setting.longSetting(LIFECYCLE_ORIGINATION_DATE, -1, -1, Setting.Property.Dynamic, Setting.Property.IndexScope); public static final Setting LIFECYCLE_PARSE_ORIGINATION_DATE_SETTING = Setting.boolSetting(LIFECYCLE_PARSE_ORIGINATION_DATE, false, Setting.Property.Dynamic, Setting.Property.IndexScope); + public static final Setting LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING = Setting.boolSetting(LIFECYCLE_HISTORY_INDEX_ENABLED, + true, Setting.Property.NodeScope); public static final Setting SLM_HISTORY_INDEX_ENABLED_SETTING = Setting.boolSetting(SLM_HISTORY_INDEX_ENABLED, true, diff --git a/x-pack/plugin/core/src/main/resources/ilm-history-ilm-policy.json b/x-pack/plugin/core/src/main/resources/ilm-history-ilm-policy.json new file mode 100644 index 0000000000000..febae00bc3608 --- /dev/null +++ b/x-pack/plugin/core/src/main/resources/ilm-history-ilm-policy.json @@ -0,0 +1,18 @@ +{ + "phases": { + "hot": { + "actions": { + "rollover": { + "max_size": "50GB", + "max_age": "30d" + } + } + }, + "delete": { + "min_age": "90d", + "actions": { + "delete": {} + } + } + } +} diff --git a/x-pack/plugin/core/src/main/resources/ilm-history.json b/x-pack/plugin/core/src/main/resources/ilm-history.json new file mode 100644 index 0000000000000..ae9c50552b385 --- /dev/null +++ b/x-pack/plugin/core/src/main/resources/ilm-history.json @@ -0,0 +1,83 @@ +{ + "index_patterns": [ + "ilm-history-${xpack.ilm_history.template.version}*" + ], + "order": 2147483647, + "settings": { + "index.number_of_shards": 1, + "index.number_of_replicas": 0, + "index.auto_expand_replicas": "0-1", + "index.lifecycle.name": "ilm-history-ilm-policy", + "index.lifecycle.rollover_alias": "ilm-history-${xpack.ilm_history.template.version}", + "index.format": 1 + }, + "mappings": { + "_doc": { + "dynamic": false, + "properties": { + "@timestamp": { + "type": "date", + "format": "epoch_millis" + }, + "policy": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "index_age":{ + "type": "long" + }, + "success": { + "type": "boolean" + }, + "state": { + "type": "object", + "dynamic": true, + "properties": { + "phase": { + "type": "keyword" + }, + "action": { + "type": "keyword" + }, + "step": { + "type": "keyword" + }, + "failed_step": { + "type": "keyword" + }, + "is_auto-retryable_error": { + "type": "keyword" + }, + "creation_date": { + "type": "date", + "format": "epoch_millis" + }, + "phase_time": { + "type": "date", + "format": "epoch_millis" + }, + "action_time": { + "type": "date", + "format": "epoch_millis" + }, + "step_time": { + "type": "date", + "format": "epoch_millis" + }, + "phase_definition": { + "type": "text" + }, + "step_info": { + "type": "text" + } + } + }, + "error_details": { + "type": "text" + } + } + } + } +} diff --git a/x-pack/plugin/ilm/qa/multi-node/build.gradle b/x-pack/plugin/ilm/qa/multi-node/build.gradle index 1fd9ccd079726..b27285e9b70c4 100644 --- a/x-pack/plugin/ilm/qa/multi-node/build.gradle +++ b/x-pack/plugin/ilm/qa/multi-node/build.gradle @@ -25,4 +25,6 @@ testClusters.integTest { setting 'xpack.ml.enabled', 'false' setting 'xpack.license.self_generated.type', 'trial' setting 'indices.lifecycle.poll_interval', '1000ms' + setting 'logger.org.elasticsearch.xpack.core.ilm', 'TRACE' + setting 'logger.org.elasticsearch.xpack.ilm', 'TRACE' } diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index f1de3fe738820..29976664bc896 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -8,14 +8,15 @@ import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; -import org.apache.log4j.LogManager; -import org.apache.log4j.Logger; import org.apache.lucene.codecs.Codec; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; import org.elasticsearch.client.RestClient; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -38,8 +39,10 @@ import org.elasticsearch.xpack.core.ilm.SetPriorityAction; import org.elasticsearch.xpack.core.ilm.ShrinkAction; import org.elasticsearch.xpack.core.ilm.ShrinkStep; +import org.elasticsearch.xpack.core.ilm.Step; import org.elasticsearch.xpack.core.ilm.Step.StepKey; import org.elasticsearch.xpack.core.ilm.TerminalPolicyStep; +import org.elasticsearch.xpack.core.ilm.WaitForRolloverReadyStep; import org.hamcrest.Matchers; import org.junit.Before; @@ -1002,6 +1005,171 @@ public void testILMRolloverOnManuallyRolledIndex() throws Exception { assertBusy(() -> assertTrue(indexExists(thirdIndex))); } + public void testHistoryIsWrittenWithSuccess() throws Exception { + String index = "index"; + + createNewSingletonPolicy("hot", new RolloverAction(null, null, 1L)); + Request createIndexTemplate = new Request("PUT", "_template/rolling_indexes"); + createIndexTemplate.setJsonEntity("{" + + "\"index_patterns\": [\""+ index + "-*\"], \n" + + " \"settings\": {\n" + + " \"number_of_shards\": 1,\n" + + " \"number_of_replicas\": 0,\n" + + " \"index.lifecycle.name\": \"" + policy+ "\",\n" + + " \"index.lifecycle.rollover_alias\": \"alias\"\n" + + " }\n" + + "}"); + client().performRequest(createIndexTemplate); + + createIndexWithSettings(index + "-1", + Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0), + true); + + // Index a document + index(client(), index + "-1", "1", "foo", "bar"); + Request refreshIndex = new Request("POST", "/" + index + "-1/_refresh"); + client().performRequest(refreshIndex); + + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "wait-for-indexing-complete"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "wait-for-follow-shard-tasks"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "pause-follower-index"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "close-follower-index"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "unfollow-follower-index"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "open-follower-index"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "wait-for-yellow-step"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "check-rollover-ready"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "attempt-rollover"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "update-rollover-lifecycle-date"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "set-indexing-complete"), 30, TimeUnit.SECONDS); + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", true, "completed"), 30, TimeUnit.SECONDS); + + assertBusy(() -> assertHistoryIsPresent(policy, index + "-000002", true, "check-rollover-ready"), 30, TimeUnit.SECONDS); + } + + public void testHistoryIsWrittenWithFailure() throws Exception { + String index = "index"; + + createNewSingletonPolicy("hot", new RolloverAction(null, null, 1L)); + Request createIndexTemplate = new Request("PUT", "_template/rolling_indexes"); + createIndexTemplate.setJsonEntity("{" + + "\"index_patterns\": [\""+ index + "-*\"], \n" + + " \"settings\": {\n" + + " \"number_of_shards\": 1,\n" + + " \"number_of_replicas\": 0,\n" + + " \"index.lifecycle.name\": \"" + policy+ "\"\n" + + " }\n" + + "}"); + client().performRequest(createIndexTemplate); + + createIndexWithSettings(index + "-1", + Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0), + false); + + // Index a document + index(client(), index + "-1", "1", "foo", "bar"); + Request refreshIndex = new Request("POST", "/" + index + "-1/_refresh"); + client().performRequest(refreshIndex); + + assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", false, "ERROR"), 30, TimeUnit.SECONDS); + } + + public void testHistoryIsWrittenWithDeletion() throws Exception { + String index = "index"; + + createNewSingletonPolicy("delete", new DeleteAction()); + Request createIndexTemplate = new Request("PUT", "_template/delete_indexes"); + createIndexTemplate.setJsonEntity("{" + + "\"index_patterns\": [\""+ index + "\"], \n" + + " \"settings\": {\n" + + " \"number_of_shards\": 1,\n" + + " \"number_of_replicas\": 0,\n" + + " \"index.lifecycle.name\": \"" + policy+ "\"\n" + + " }\n" + + "}"); + client().performRequest(createIndexTemplate); + + // Index should be created and then deleted by ILM + createIndexWithSettings(index, Settings.builder(), false); + + assertBusy(() -> { + logger.info("--> checking for index deletion..."); + Request existCheck = new Request("HEAD", "/" + index); + Response resp = client().performRequest(existCheck); + assertThat(resp.getStatusLine().getStatusCode(), equalTo(404)); + }); + + assertBusy(() -> { + assertHistoryIsPresent(policy, index, true, "delete", "delete", "wait-for-shard-history-leases"); + assertHistoryIsPresent(policy, index, true, "delete", "delete", "complete"); + }, 30, TimeUnit.SECONDS); + } + + // This method should be called inside an assertBusy, it has no retry logic of its own + private void assertHistoryIsPresent(String policyName, String indexName, boolean success, String stepName) throws IOException { + assertHistoryIsPresent(policyName, indexName, success, null, null, stepName); + } + + // This method should be called inside an assertBusy, it has no retry logic of its own + private void assertHistoryIsPresent(String policyName, String indexName, boolean success, + @Nullable String phase, @Nullable String action, String stepName) throws IOException { + logger.info("--> checking for history item [{}], [{}], success: [{}], phase: [{}], action: [{}], step: [{}]", + policyName, indexName, success, phase, action, stepName); + final Request historySearchRequest = new Request("GET", "ilm-history*/_search"); + historySearchRequest.setJsonEntity("{\n" + + " \"query\": {\n" + + " \"bool\": {\n" + + " \"must\": [\n" + + " {\n" + + " \"term\": {\n" + + " \"policy\": \"" + policyName + "\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"term\": {\n" + + " \"success\": " + success + "\n" + + " }\n" + + " },\n" + + " {\n" + + " \"term\": {\n" + + " \"index\": \"" + indexName + "\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"term\": {\n" + + " \"state.step\": \"" + stepName + "\"\n" + + " }\n" + + " }\n" + + (phase == null ? "" : ",{\"term\": {\"state.phase\": \"" + phase + "\"}}") + + (action == null ? "" : ",{\"term\": {\"state.action\": \"" + action + "\"}}") + + " ]\n" + + " }\n" + + " }\n" + + "}"); + Response historyResponse; + try { + historyResponse = client().performRequest(historySearchRequest); + Map historyResponseMap; + try (InputStream is = historyResponse.getEntity().getContent()) { + historyResponseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); + } + logger.info("--> history response: {}", historyResponseMap); + assertThat((int)((Map) ((Map) historyResponseMap.get("hits")).get("total")).get("value"), + greaterThanOrEqualTo(1)); + } catch (ResponseException e) { + // Throw AssertionError instead of an exception if the search fails so that assertBusy works as expected + logger.error(e); + fail("failed to perform search:" + e.getMessage()); + } + + // Finally, check that the history index is in a good state + Step.StepKey stepKey = getStepKeyForIndex("ilm-history-1-000001"); + assertEquals("hot", stepKey.getPhase()); + assertEquals(RolloverAction.NAME, stepKey.getAction()); + assertEquals(WaitForRolloverReadyStep.NAME, stepKey.getName()); + } + private void createFullPolicy(TimeValue hotTime) throws IOException { Map hotActions = new HashMap<>(); hotActions.put(SetPriorityAction.NAME, new SetPriorityAction(100)); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java index 46364f7cb4021..b97944fe67bf7 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java @@ -12,10 +12,13 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.index.Index; import org.elasticsearch.xpack.core.ilm.ClusterStateActionStep; import org.elasticsearch.xpack.core.ilm.ClusterStateWaitStep; +import org.elasticsearch.xpack.core.ilm.ErrorStep; +import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; import org.elasticsearch.xpack.core.ilm.Step; import org.elasticsearch.xpack.core.ilm.TerminalPolicyStep; @@ -29,8 +32,9 @@ public class ExecuteStepsUpdateTask extends ClusterStateUpdateTask { private final Step startStep; private final PolicyStepsRegistry policyStepsRegistry; private final IndexLifecycleRunner lifecycleRunner; - private LongSupplier nowSupplier; + private final LongSupplier nowSupplier; private Step.StepKey nextStepKey = null; + private Exception failure = null; public ExecuteStepsUpdateTask(String policy, Index index, Step startStep, PolicyStepsRegistry policyStepsRegistry, IndexLifecycleRunner lifecycleRunner, LongSupplier nowSupplier) { @@ -115,7 +119,7 @@ public ClusterState execute(final ClusterState currentState) throws IOException // wait for the next trigger to evaluate the // condition again logger.trace("[{}] waiting for cluster state step condition ({}) [{}], next: [{}]", - index.getName(), currentStep.getClass().getSimpleName(), currentStep.getKey(), currentStep.getNextStepKey()); + index.getName(), currentStep.getClass().getSimpleName(), currentStep.getKey(), nextStepKey); ClusterStateWaitStep.Result result; try { result = ((ClusterStateWaitStep) currentStep).isConditionMet(index, state); @@ -124,22 +128,25 @@ public ClusterState execute(final ClusterState currentState) throws IOException } if (result.isComplete()) { logger.trace("[{}] cluster state step condition met successfully ({}) [{}], moving to next step {}", - index.getName(), currentStep.getClass().getSimpleName(), currentStep.getKey(), currentStep.getNextStepKey()); - if (currentStep.getNextStepKey() == null) { + index.getName(), currentStep.getClass().getSimpleName(), currentStep.getKey(), nextStepKey); + if (nextStepKey == null) { return state; } else { state = IndexLifecycleTransition.moveClusterStateToStep(index, state, - currentStep.getNextStepKey(), nowSupplier, policyStepsRegistry,false); + nextStepKey, nowSupplier, policyStepsRegistry,false); } } else { - logger.trace("[{}] condition not met ({}) [{}], returning existing state", - index.getName(), currentStep.getClass().getSimpleName(), currentStep.getKey()); + final ToXContentObject stepInfo = result.getInfomationContext(); + if (logger.isTraceEnabled()) { + logger.trace("[{}] condition not met ({}) [{}], returning existing state (info: {})", + index.getName(), currentStep.getClass().getSimpleName(), currentStep.getKey(), + Strings.toString(stepInfo)); + } // We may have executed a step and set "nextStepKey" to // a value, but in this case, since the condition was // not met, we can't advance any way, so don't attempt // to run the current step nextStepKey = null; - ToXContentObject stepInfo = result.getInfomationContext(); if (stepInfo == null) { return state; } else { @@ -169,13 +176,23 @@ public ClusterState execute(final ClusterState currentState) throws IOException public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { if (oldState.equals(newState) == false) { IndexMetaData indexMetaData = newState.metaData().index(index); - if (nextStepKey != null && nextStepKey != TerminalPolicyStep.KEY && indexMetaData != null) { - logger.trace("[{}] step sequence starting with {} has completed, running next step {} if it is an async action", - index.getName(), startStep.getKey(), nextStepKey); - // After the cluster state has been processed and we have moved - // to a new step, we need to conditionally execute the step iff - // it is an `AsyncAction` so that it is executed exactly once. - lifecycleRunner.maybeRunAsyncAction(newState, indexMetaData, policy, nextStepKey); + if (indexMetaData != null) { + + LifecycleExecutionState exState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); + if (ErrorStep.NAME.equals(exState.getStep()) && this.failure != null) { + lifecycleRunner.registerFailedOperation(indexMetaData, failure); + } else { + lifecycleRunner.registerSuccessfulOperation(indexMetaData); + } + + if (nextStepKey != null && nextStepKey != TerminalPolicyStep.KEY) { + logger.trace("[{}] step sequence starting with {} has completed, running next step {} if it is an async action", + index.getName(), startStep.getKey(), nextStepKey); + // After the cluster state has been processed and we have moved + // to a new step, we need to conditionally execute the step iff + // it is an `AsyncAction` so that it is executed exactly once. + lifecycleRunner.maybeRunAsyncAction(newState, indexMetaData, policy, nextStepKey); + } } } } @@ -187,10 +204,9 @@ public void onFailure(String source, Exception e) { } private ClusterState moveToErrorStep(final ClusterState state, Step.StepKey currentStepKey, Exception cause) throws IOException { + this.failure = cause; logger.error("policy [{}] for index [{}] failed on cluster state step [{}]. Moving to ERROR step", policy, index.getName(), currentStepKey); - MoveToErrorStepUpdateTask moveToErrorStepUpdateTask = new MoveToErrorStepUpdateTask(index, policy, currentStepKey, cause, - nowSupplier, policyStepsRegistry::getStep); - return moveToErrorStepUpdateTask.execute(state); + return IndexLifecycleTransition.moveClusterStateToErrorStep(index, state, cause, nowSupplier, policyStepsRegistry::getStep); } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java index 394174ed673f6..4051b291b9801 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java @@ -94,6 +94,8 @@ import org.elasticsearch.xpack.ilm.action.TransportRetryAction; import org.elasticsearch.xpack.ilm.action.TransportStartILMAction; import org.elasticsearch.xpack.ilm.action.TransportStopILMAction; +import org.elasticsearch.xpack.ilm.history.ILMHistoryStore; +import org.elasticsearch.xpack.ilm.history.ILMHistoryTemplateRegistry; import org.elasticsearch.xpack.slm.SLMInfoTransportAction; import org.elasticsearch.xpack.slm.SLMUsageTransportAction; import org.elasticsearch.xpack.slm.SnapshotLifecycleService; @@ -132,6 +134,7 @@ public class IndexLifecycle extends Plugin implements ActionPlugin { private final SetOnce indexLifecycleInitialisationService = new SetOnce<>(); + private final SetOnce ilmHistoryStore = new SetOnce<>(); private final SetOnce snapshotLifecycleService = new SetOnce<>(); private final SetOnce snapshotRetentionService = new SetOnce<>(); private final SetOnce snapshotHistoryStore = new SetOnce<>(); @@ -158,6 +161,7 @@ public List> getSettings() { LifecycleSettings.LIFECYCLE_ORIGINATION_DATE_SETTING, LifecycleSettings.LIFECYCLE_PARSE_ORIGINATION_DATE_SETTING, LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE_SETTING, + LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING, RolloverAction.LIFECYCLE_ROLLOVER_ALIAS_SETTING, LifecycleSettings.SLM_HISTORY_INDEX_ENABLED_SETTING, LifecycleSettings.SLM_RETENTION_SCHEDULE_SETTING, @@ -171,8 +175,13 @@ public Collection createComponents(Client client, ClusterService cluster NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry) { final List components = new ArrayList<>(); if (ilmEnabled) { + // This registers a cluster state listener, so appears unused but is not. + @SuppressWarnings("unused") + ILMHistoryTemplateRegistry ilmTemplateRegistry = + new ILMHistoryTemplateRegistry(settings, clusterService, threadPool, client, xContentRegistry); + ilmHistoryStore.set(new ILMHistoryStore(settings, new OriginSettingClient(client, INDEX_LIFECYCLE_ORIGIN), clusterService)); indexLifecycleInitialisationService.set(new IndexLifecycleService(settings, client, clusterService, threadPool, - getClock(), System::currentTimeMillis, xContentRegistry)); + getClock(), System::currentTimeMillis, xContentRegistry, ilmHistoryStore.get())); components.add(indexLifecycleInitialisationService.get()); } if (slmEnabled) { @@ -308,7 +317,8 @@ public void onIndexModule(IndexModule indexModule) { @Override public void close() { try { - IOUtils.close(indexLifecycleInitialisationService.get(), snapshotLifecycleService.get(), snapshotRetentionService.get()); + IOUtils.close(indexLifecycleInitialisationService.get(), ilmHistoryStore.get(), + snapshotLifecycleService.get(), snapshotRetentionService.get()); } catch (IOException e) { throw new ElasticsearchException("unable to close index lifecycle services", e); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java index 5d64c35498a3d..736d5decc1123 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java @@ -13,6 +13,7 @@ import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.index.Index; @@ -23,10 +24,13 @@ import org.elasticsearch.xpack.core.ilm.ClusterStateWaitStep; import org.elasticsearch.xpack.core.ilm.ErrorStep; import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; +import org.elasticsearch.xpack.core.ilm.LifecycleSettings; import org.elasticsearch.xpack.core.ilm.PhaseCompleteStep; import org.elasticsearch.xpack.core.ilm.Step; import org.elasticsearch.xpack.core.ilm.Step.StepKey; import org.elasticsearch.xpack.core.ilm.TerminalPolicyStep; +import org.elasticsearch.xpack.ilm.history.ILMHistoryItem; +import org.elasticsearch.xpack.ilm.history.ILMHistoryStore; import java.util.function.LongSupplier; @@ -35,13 +39,15 @@ class IndexLifecycleRunner { private static final Logger logger = LogManager.getLogger(IndexLifecycleRunner.class); private final ThreadPool threadPool; - private PolicyStepsRegistry stepRegistry; - private ClusterService clusterService; - private LongSupplier nowSupplier; + private final ClusterService clusterService; + private final PolicyStepsRegistry stepRegistry; + private final ILMHistoryStore ilmHistoryStore; + private final LongSupplier nowSupplier; - IndexLifecycleRunner(PolicyStepsRegistry stepRegistry, ClusterService clusterService, + IndexLifecycleRunner(PolicyStepsRegistry stepRegistry, ILMHistoryStore ilmHistoryStore, ClusterService clusterService, ThreadPool threadPool, LongSupplier nowSupplier) { this.stepRegistry = stepRegistry; + this.ilmHistoryStore = ilmHistoryStore; this.clusterService = clusterService; this.nowSupplier = nowSupplier; this.threadPool = threadPool; @@ -62,17 +68,29 @@ static Step getCurrentStep(PolicyStepsRegistry stepRegistry, String policy, Inde } /** - * Return true or false depending on whether the index is ready to be in {@code phase} + * Calculate the index's origination time (in milliseconds) based on its + * metadata. Returns null if there is no lifecycle date and the origination + * date is not set. */ - boolean isReadyToTransitionToThisPhase(final String policy, final IndexMetaData indexMetaData, final String phase) { + @Nullable + private static Long calculateOriginationMillis(final IndexMetaData indexMetaData) { LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetaData); Long originationDate = indexMetaData.getSettings().getAsLong(LIFECYCLE_ORIGINATION_DATE, -1L); if (lifecycleState.getLifecycleDate() == null && originationDate == -1L) { - logger.trace("no index creation or origination date has been set yet"); + return null; + } + return originationDate == -1L ? lifecycleState.getLifecycleDate() : originationDate; + } + + /** + * Return true or false depending on whether the index is ready to be in {@code phase} + */ + boolean isReadyToTransitionToThisPhase(final String policy, final IndexMetaData indexMetaData, final String phase) { + final Long lifecycleDate = calculateOriginationMillis(indexMetaData); + if (lifecycleDate == null) { + logger.trace("[{}] no index creation or origination date has been set yet", indexMetaData.getIndex().getName()); return true; } - final Long lifecycleDate = originationDate != -1L ? originationDate : lifecycleState.getLifecycleDate(); - assert lifecycleDate != null && lifecycleDate >= 0 : "expected index to have a lifecycle date but it did not"; final TimeValue after = stepRegistry.getIndexAgeForPhase(policy, phase); final long now = nowSupplier.getAsLong(); final TimeValue age = new TimeValue(now - lifecycleDate); @@ -221,19 +239,26 @@ void maybeRunAsyncAction(ClusterState currentState, IndexMetaData indexMetaData, ((AsyncActionStep) currentStep).performAction(indexMetaData, currentState, new ClusterStateObserver(clusterService, null, logger, threadPool.getThreadContext()), new AsyncActionStep.Listener() { - @Override - public void onResponse(boolean complete) { - logger.trace("cs-change-async-action-callback, [{}], current-step: {}", index, currentStep.getKey()); - if (complete && ((AsyncActionStep) currentStep).indexSurvives()) { - moveToStep(indexMetaData.getIndex(), policy, currentStep.getKey(), currentStep.getNextStepKey()); + @Override + public void onResponse(boolean complete) { + logger.trace("cs-change-async-action-callback, [{}], current-step: {}", index, currentStep.getKey()); + if (complete) { + if (((AsyncActionStep) currentStep).indexSurvives()) { + moveToStep(indexMetaData.getIndex(), policy, currentStep.getKey(), currentStep.getNextStepKey()); + } else { + // Delete needs special handling, because after this step we + // will no longer have access to any information about the + // index since it will be... deleted. + registerDeleteOperation(indexMetaData); + } + } } - } - @Override - public void onFailure(Exception e) { - moveToErrorStep(indexMetaData.getIndex(), policy, currentStep.getKey(), e); - } - }); + @Override + public void onFailure(Exception e) { + moveToErrorStep(indexMetaData.getIndex(), policy, currentStep.getKey(), e); + } + }); } else { logger.trace("[{}] ignoring non async action step execution from step transition [{}]", index, currentStep.getKey()); } @@ -298,6 +323,7 @@ private void moveToStep(Index index, String policy, Step.StepKey currentStepKey, new MoveToNextStepUpdateTask(index, policy, currentStepKey, newStepKey, nowSupplier, stepRegistry, clusterState -> { IndexMetaData indexMetaData = clusterState.metaData().index(index); + registerSuccessfulOperation(indexMetaData); if (newStepKey != null && newStepKey != TerminalPolicyStep.KEY && indexMetaData != null) { maybeRunAsyncAction(clusterState, indexMetaData, policy, newStepKey); } @@ -311,7 +337,10 @@ private void moveToErrorStep(Index index, String policy, Step.StepKey currentSte logger.error(new ParameterizedMessage("policy [{}] for index [{}] failed on step [{}]. Moving to ERROR step", policy, index.getName(), currentStepKey), e); clusterService.submitStateUpdateTask("ilm-move-to-error-step", - new MoveToErrorStepUpdateTask(index, policy, currentStepKey, e, nowSupplier, stepRegistry::getStep)); + new MoveToErrorStepUpdateTask(index, policy, currentStepKey, e, nowSupplier, stepRegistry::getStep, clusterState -> { + IndexMetaData indexMetaData = clusterState.metaData().index(index); + registerFailedOperation(indexMetaData, e); + })); } /** @@ -343,4 +372,61 @@ private void markPolicyRetrievalError(String policyName, Index index, LifecycleE setStepInfo(index, policyName, LifecycleExecutionState.getCurrentStepKey(executionState), new SetStepInfoUpdateTask.ExceptionWrapper(e)); } + + /** + * For the given index metadata, register (index a document) that the index has transitioned + * successfully into this new state using the {@link ILMHistoryStore} + */ + void registerSuccessfulOperation(IndexMetaData indexMetaData) { + if (indexMetaData == null) { + // This index may have been deleted and has no metadata, so ignore it + return; + } + Long origination = calculateOriginationMillis(indexMetaData); + ilmHistoryStore.putAsync( + ILMHistoryItem.success(indexMetaData.getIndex().getName(), + LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetaData.getSettings()), + nowSupplier.getAsLong(), + origination == null ? null : (nowSupplier.getAsLong() - origination), + LifecycleExecutionState.fromIndexMetadata(indexMetaData))); + } + + /** + * For the given index metadata, register (index a document) that the index + * has been deleted by ILM using the {@link ILMHistoryStore} + */ + void registerDeleteOperation(IndexMetaData metadataBeforeDeletion) { + if (metadataBeforeDeletion == null) { + throw new IllegalStateException("cannot register deletion of an index that did not previously exist"); + } + Long origination = calculateOriginationMillis(metadataBeforeDeletion); + ilmHistoryStore.putAsync( + ILMHistoryItem.success(metadataBeforeDeletion.getIndex().getName(), + LifecycleSettings.LIFECYCLE_NAME_SETTING.get(metadataBeforeDeletion.getSettings()), + nowSupplier.getAsLong(), + origination == null ? null : (nowSupplier.getAsLong() - origination), + LifecycleExecutionState.builder(LifecycleExecutionState.fromIndexMetadata(metadataBeforeDeletion)) + // Register that the delete phase is now "complete" + .setStep(PhaseCompleteStep.NAME) + .build())); + } + + /** + * For the given index metadata, register (index a document) that the index has transitioned + * into the ERROR state using the {@link ILMHistoryStore} + */ + void registerFailedOperation(IndexMetaData indexMetaData, Exception failure) { + if (indexMetaData == null) { + // This index may have been deleted and has no metadata, so ignore it + return; + } + Long origination = calculateOriginationMillis(indexMetaData); + ilmHistoryStore.putAsync( + ILMHistoryItem.failure(indexMetaData.getIndex().getName(), + LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetaData.getSettings()), + nowSupplier.getAsLong(), + origination == null ? null : (nowSupplier.getAsLong() - origination), + LifecycleExecutionState.fromIndexMetadata(indexMetaData), + failure)); + } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java index f116f9de08743..a5ae4d4673a77 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java @@ -35,6 +35,7 @@ import org.elasticsearch.xpack.core.ilm.ShrinkStep; import org.elasticsearch.xpack.core.ilm.Step.StepKey; import org.elasticsearch.xpack.core.scheduler.SchedulerEngine; +import org.elasticsearch.xpack.ilm.history.ILMHistoryStore; import java.io.Closeable; import java.time.Clock; @@ -59,21 +60,24 @@ public class IndexLifecycleService private final Clock clock; private final PolicyStepsRegistry policyRegistry; private final IndexLifecycleRunner lifecycleRunner; + private final ILMHistoryStore ilmHistoryStore; private final Settings settings; private ClusterService clusterService; private LongSupplier nowSupplier; private SchedulerEngine.Job scheduledJob; public IndexLifecycleService(Settings settings, Client client, ClusterService clusterService, ThreadPool threadPool, Clock clock, - LongSupplier nowSupplier, NamedXContentRegistry xContentRegistry) { + LongSupplier nowSupplier, NamedXContentRegistry xContentRegistry, + ILMHistoryStore ilmHistoryStore) { super(); this.settings = settings; this.clusterService = clusterService; this.clock = clock; this.nowSupplier = nowSupplier; this.scheduledJob = null; + this.ilmHistoryStore = ilmHistoryStore; this.policyRegistry = new PolicyStepsRegistry(xContentRegistry, client); - this.lifecycleRunner = new IndexLifecycleRunner(policyRegistry, clusterService, threadPool, nowSupplier); + this.lifecycleRunner = new IndexLifecycleRunner(policyRegistry, ilmHistoryStore, clusterService, threadPool, nowSupplier); this.pollInterval = LifecycleSettings.LIFECYCLE_POLL_INTERVAL_SETTING.get(settings); clusterService.addStateApplier(this); clusterService.addListener(this); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTask.java index 1b80e070c5552..ae05d21021339 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTask.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.LongSupplier; public class MoveToErrorStepUpdateTask extends ClusterStateUpdateTask { @@ -24,17 +25,20 @@ public class MoveToErrorStepUpdateTask extends ClusterStateUpdateTask { private final String policy; private final Step.StepKey currentStepKey; private final BiFunction stepLookupFunction; + private final Consumer stateChangeConsumer; private LongSupplier nowSupplier; private Exception cause; public MoveToErrorStepUpdateTask(Index index, String policy, Step.StepKey currentStepKey, Exception cause, LongSupplier nowSupplier, - BiFunction stepLookupFunction) { + BiFunction stepLookupFunction, + Consumer stateChangeConsumer) { this.index = index; this.policy = policy; this.currentStepKey = currentStepKey; this.cause = cause; this.nowSupplier = nowSupplier; this.stepLookupFunction = stepLookupFunction; + this.stateChangeConsumer = stateChangeConsumer; } Index getIndex() { @@ -73,6 +77,13 @@ public ClusterState execute(ClusterState currentState) throws IOException { } } + @Override + public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { + if (newState.equals(oldState) == false) { + stateChangeConsumer.accept(newState); + } + } + @Override public void onFailure(String source, Exception e) { throw new ElasticsearchException("policy [" + policy + "] for index [" + index.getName() diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItem.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItem.java new file mode 100644 index 0000000000000..e972613e3e901 --- /dev/null +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItem.java @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.ilm.history; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; + +import java.io.IOException; +import java.util.Collections; +import java.util.Objects; + +import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE; + +/** + * The {@link ILMHistoryItem} class encapsulates the state of an index at a point in time. It should + * be constructed when an index has transitioned into a new step. Construction is done through the + * {@link #success(String, String, long, Long, LifecycleExecutionState)} and + * {@link #failure(String, String, long, Long, LifecycleExecutionState, Exception)} methods. + */ +public class ILMHistoryItem implements ToXContentObject { + private static final ParseField INDEX = new ParseField("index"); + private static final ParseField POLICY = new ParseField("policy"); + private static final ParseField TIMESTAMP = new ParseField("@timestamp"); + private static final ParseField INDEX_AGE = new ParseField("index_age"); + private static final ParseField SUCCESS = new ParseField("success"); + private static final ParseField EXECUTION_STATE = new ParseField("state"); + private static final ParseField ERROR = new ParseField("error_details"); + + private final String index; + private final String policyId; + private final long timestamp; + @Nullable + private final Long indexAge; + private final boolean success; + @Nullable + private final LifecycleExecutionState executionState; + @Nullable + private final String errorDetails; + + private ILMHistoryItem(String index, String policyId, long timestamp, @Nullable Long indexAge, boolean success, + @Nullable LifecycleExecutionState executionState, @Nullable String errorDetails) { + this.index = index; + this.policyId = policyId; + this.timestamp = timestamp; + this.indexAge = indexAge; + this.success = success; + this.executionState = executionState; + this.errorDetails = errorDetails; + } + + public static ILMHistoryItem success(String index, String policyId, long timestamp, @Nullable Long indexAge, + @Nullable LifecycleExecutionState executionState) { + return new ILMHistoryItem(index, policyId, timestamp, indexAge, true, executionState, null); + } + + public static ILMHistoryItem failure(String index, String policyId, long timestamp, @Nullable Long indexAge, + @Nullable LifecycleExecutionState executionState, Exception error) { + Objects.requireNonNull(error, "ILM failures require an attached exception"); + return new ILMHistoryItem(index, policyId, timestamp, indexAge, false, executionState, exceptionToString(error)); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(INDEX.getPreferredName(), index); + builder.field(POLICY.getPreferredName(), policyId); + builder.field(TIMESTAMP.getPreferredName(), timestamp); + if (indexAge != null) { + builder.field(INDEX_AGE.getPreferredName(), indexAge); + } + builder.field(SUCCESS.getPreferredName(), success); + if (executionState != null) { + builder.field(EXECUTION_STATE.getPreferredName(), executionState.asMap()); + } + if (errorDetails != null) { + builder.field(ERROR.getPreferredName(), errorDetails); + } + builder.endObject(); + return builder; + } + + private static String exceptionToString(Exception exception) { + Params stacktraceParams = new MapParams(Collections.singletonMap(REST_EXCEPTION_SKIP_STACK_TRACE, "false")); + String exceptionString; + try (XContentBuilder causeXContentBuilder = JsonXContent.contentBuilder()) { + causeXContentBuilder.startObject(); + ElasticsearchException.generateThrowableXContent(causeXContentBuilder, stacktraceParams, exception); + causeXContentBuilder.endObject(); + exceptionString = BytesReference.bytes(causeXContentBuilder).utf8ToString(); + } catch (IOException e) { + // In the unlikely case that we cannot generate an exception string, + // try the best way can to encapsulate the error(s) with at least + // the message + exceptionString = "unable to generate the ILM error details due to: " + e.getMessage() + + "; the ILM error was: " + exception.getMessage(); + } + return exceptionString; + } + + @Override + public String toString() { + return Strings.toString(this); + } +} diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java new file mode 100644 index 0000000000000..ab8168de4b28d --- /dev/null +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.ilm.history; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ResourceAlreadyExistsException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.alias.Alias; +import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; +import org.elasticsearch.action.bulk.BackoffPolicy; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkProcessor; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.OriginSettingClient; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeUnit; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.elasticsearch.xpack.core.ClientHelper.INDEX_LIFECYCLE_ORIGIN; +import static org.elasticsearch.xpack.core.ilm.LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING; +import static org.elasticsearch.xpack.ilm.history.ILMHistoryTemplateRegistry.INDEX_TEMPLATE_VERSION; + +/** + * The {@link ILMHistoryStore} handles indexing {@link ILMHistoryItem} documents into the + * appropriate index. It sets up a {@link BulkProcessor} for indexing in bulk, and handles creation + * of the index/alias as needed for ILM policies. + */ +public class ILMHistoryStore implements Closeable { + private static final Logger logger = LogManager.getLogger(ILMHistoryStore.class); + + public static final String ILM_HISTORY_INDEX_PREFIX = "ilm-history-" + INDEX_TEMPLATE_VERSION + "-"; + public static final String ILM_HISTORY_ALIAS = "ilm-history-" + INDEX_TEMPLATE_VERSION; + + private final Client client; + private final ClusterService clusterService; + private final boolean ilmHistoryEnabled; + private final BulkProcessor processor; + + public ILMHistoryStore(Settings nodeSettings, Client client, ClusterService clusterService) { + this.client = client; + this.clusterService = clusterService; + ilmHistoryEnabled = LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING.get(nodeSettings); + + this.processor = BulkProcessor.builder( + new OriginSettingClient(client, INDEX_LIFECYCLE_ORIGIN)::bulk, + new BulkProcessor.Listener() { + @Override + public void beforeBulk(long executionId, BulkRequest request) { } + + @Override + public void afterBulk(long executionId, BulkRequest request, BulkResponse response) { + long items = request.numberOfActions(); + logger.trace("indexed [{}] items into ILM history index", items); + if (response.hasFailures()) { + Map failures = Arrays.stream(response.getItems()) + .filter(BulkItemResponse::isFailed) + .collect(Collectors.toMap(BulkItemResponse::getId, BulkItemResponse::getFailureMessage)); + logger.error("failures: [{}]", failures); + } + } + + @Override + public void afterBulk(long executionId, BulkRequest request, Throwable failure) { + long items = request.numberOfActions(); + logger.error(new ParameterizedMessage("failed to index {} items into ILM history index", items), failure); + } + }) + .setBulkActions(100) + .setBulkSize(new ByteSizeValue(5, ByteSizeUnit.MB)) + .setFlushInterval(TimeValue.timeValueSeconds(5)) + .setConcurrentRequests(1) + .setBackoffPolicy(BackoffPolicy.exponentialBackoff(TimeValue.timeValueMillis(1000), 3)) + .build(); + } + + /** + * Attempts to asynchronously index an ILM history entry + */ + public void putAsync(ILMHistoryItem item) { + if (ilmHistoryEnabled == false) { + logger.trace("not recording ILM history item because [{}] is [false]: [{}]", + LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING.getKey(), item); + return; + } + logger.trace("about to index ILM history item in index [{}]: [{}]", ILM_HISTORY_ALIAS, item); + ensureHistoryIndex(client, clusterService.state(), ActionListener.wrap(createdIndex -> { + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + item.toXContent(builder, ToXContent.EMPTY_PARAMS); + IndexRequest request = new IndexRequest(ILM_HISTORY_ALIAS).source(builder); + processor.add(request); + } catch (IOException exception) { + logger.error(new ParameterizedMessage("failed to index ILM history item in index [{}]: [{}]", + ILM_HISTORY_ALIAS, item), exception); + } + }, ex -> logger.error(new ParameterizedMessage("failed to ensure ILM history index exists, not indexing history item [{}]", + item), ex))); + } + + /** + * Checks if the ILM history index exists, and if not, creates it. + * + * @param client The client to use to create the index if needed + * @param state The current cluster state, to determine if the alias exists + * @param listener Called after the index has been created. `onResponse` called with `true` if the index was created, + * `false` if it already existed. + */ + static void ensureHistoryIndex(Client client, ClusterState state, ActionListener listener) { + final String initialHistoryIndexName = ILM_HISTORY_INDEX_PREFIX + "000001"; + final AliasOrIndex ilmHistory = state.metaData().getAliasAndIndexLookup().get(ILM_HISTORY_ALIAS); + final AliasOrIndex initialHistoryIndex = state.metaData().getAliasAndIndexLookup().get(initialHistoryIndexName); + + if (ilmHistory == null && initialHistoryIndex == null) { + // No alias or index exists with the expected names, so create the index with appropriate alias + client.admin().indices().prepareCreate(initialHistoryIndexName) + .setWaitForActiveShards(1) + .addAlias(new Alias(ILM_HISTORY_ALIAS) + .writeIndex(true)) + .execute(new ActionListener() { + @Override + public void onResponse(CreateIndexResponse response) { + listener.onResponse(true); + } + + @Override + public void onFailure(Exception e) { + if (e instanceof ResourceAlreadyExistsException) { + // The index didn't exist before we made the call, there was probably a race - just ignore this + logger.debug("index [{}] was created after checking for its existence, likely due to a concurrent call", + initialHistoryIndexName); + listener.onResponse(false); + } else { + listener.onFailure(e); + } + } + }); + } else if (ilmHistory == null) { + // alias does not exist but initial index does, something is broken + listener.onFailure(new IllegalStateException("ILM history index [" + initialHistoryIndexName + + "] already exists but does not have alias [" + ILM_HISTORY_ALIAS + "]")); + } else if (ilmHistory.isAlias() && ilmHistory instanceof AliasOrIndex.Alias) { + if (((AliasOrIndex.Alias) ilmHistory).getWriteIndex() != null) { + // The alias exists and has a write index, so we're good + listener.onResponse(false); + } else { + // The alias does not have a write index, so we can't index into it + listener.onFailure(new IllegalStateException("ILM history alias [" + ILM_HISTORY_ALIAS + "does not have a write index")); + } + } else if (ilmHistory.isAlias() == false) { + // This is not an alias, error out + listener.onFailure(new IllegalStateException("ILM history alias [" + ILM_HISTORY_ALIAS + + "] already exists as concrete index")); + } else { + logger.error("unexpected IndexOrAlias for [{}]: [{}]", ILM_HISTORY_ALIAS, ilmHistory); + assert false : ILM_HISTORY_ALIAS + " cannot be both an alias and not an alias simultaneously"; + } + } + + @Override + public void close() { + try { + processor.awaitClose(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.warn("failed to shut down ILM history bulk processor after 10 seconds", e); + } + } +} diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryTemplateRegistry.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryTemplateRegistry.java new file mode 100644 index 0000000000000..21b2d16afdc83 --- /dev/null +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryTemplateRegistry.java @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.ilm.history; + +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.ClientHelper; +import org.elasticsearch.xpack.core.ilm.LifecycleSettings; +import org.elasticsearch.xpack.core.template.IndexTemplateConfig; +import org.elasticsearch.xpack.core.template.IndexTemplateRegistry; +import org.elasticsearch.xpack.core.template.LifecyclePolicyConfig; + +import java.util.Collections; +import java.util.List; + +/** + * The {@link ILMHistoryTemplateRegistry} class sets up and configures an ILM policy and index + * template for the ILM history indices (ilm-history-N-00000M). + */ +public class ILMHistoryTemplateRegistry extends IndexTemplateRegistry { + // history (please add a comment why you increased the version here) + // version 1: initial + public static final String INDEX_TEMPLATE_VERSION = "1"; + + public static final String ILM_TEMPLATE_VERSION_VARIABLE = "xpack.ilm_history.template.version"; + public static final String ILM_TEMPLATE_NAME = "ilm-history"; + + public static final String ILM_POLICY_NAME = "ilm-history-ilm-policy"; + + public static final IndexTemplateConfig TEMPLATE_ILM_HISTORY = new IndexTemplateConfig( + ILM_TEMPLATE_NAME, + "/ilm-history.json", + INDEX_TEMPLATE_VERSION, + ILM_TEMPLATE_VERSION_VARIABLE + ); + + public static final LifecyclePolicyConfig ILM_HISTORY_POLICY = new LifecyclePolicyConfig( + ILM_POLICY_NAME, + "/ilm-history-ilm-policy.json" + ); + + private final boolean ilmHistoryEnabled; + + public ILMHistoryTemplateRegistry(Settings nodeSettings, ClusterService clusterService, + ThreadPool threadPool, Client client, + NamedXContentRegistry xContentRegistry) { + super(nodeSettings, clusterService, threadPool, client, xContentRegistry); + this.ilmHistoryEnabled = LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING.get(nodeSettings); + } + + @Override + protected List getTemplateConfigs() { + if (this.ilmHistoryEnabled) { + return Collections.singletonList(TEMPLATE_ILM_HISTORY); + } else { + return Collections.emptyList(); + } + } + + @Override + protected List getPolicyConfigs() { + if (this.ilmHistoryEnabled) { + return Collections.singletonList(ILM_HISTORY_POLICY); + } else { + return Collections.emptyList(); + } + } + + @Override + protected String getOrigin() { + return ClientHelper.INDEX_LIFECYCLE_ORIGIN; + } +} diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java index b49d0870bdaa3..22f40913997a3 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java @@ -106,7 +106,8 @@ protected Settings nodeSettings(int nodeOrdinal) { settings.put(XPackSettings.LOGSTASH_ENABLED.getKey(), false); settings.put(LifecycleSettings.LIFECYCLE_POLL_INTERVAL, "1s"); - // This is necessary to prevent SLM installing a lifecycle policy, these tests assume a blank slate + // This is necessary to prevent ILM and SLM installing a lifecycle policy, these tests assume a blank slate + settings.put(LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED, false); settings.put(LifecycleSettings.SLM_HISTORY_INDEX_ENABLED_SETTING.getKey(), false); return settings.build(); } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java index 2580e2970e521..130a77cf853dd 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java @@ -55,6 +55,8 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import org.elasticsearch.xpack.core.ilm.TerminalPolicyStep; import org.elasticsearch.xpack.core.ilm.WaitForRolloverReadyStep; +import org.elasticsearch.xpack.ilm.history.ILMHistoryItem; +import org.elasticsearch.xpack.ilm.history.ILMHistoryStore; import org.junit.After; import org.junit.Before; import org.mockito.ArgumentMatcher; @@ -91,6 +93,8 @@ public class IndexLifecycleRunnerTests extends ESTestCase { private static final NamedXContentRegistry REGISTRY; private ThreadPool threadPool; + private Client noopClient; + private NoOpHistoryStore historyStore; static { try (IndexLifecycle indexLifecycle = new IndexLifecycle(Settings.EMPTY)) { @@ -100,12 +104,16 @@ public class IndexLifecycleRunnerTests extends ESTestCase { } @Before - public void prepareThreadPool() { + public void prepare() { threadPool = new TestThreadPool("test"); + noopClient = new NoOpClient(threadPool); + historyStore = new NoOpHistoryStore(); } @After public void shutdown() { + historyStore.close(); + noopClient.close(); threadPool.shutdownNow(); } @@ -114,7 +122,7 @@ public void testRunPolicyTerminalPolicyStep() { TerminalPolicyStep step = TerminalPolicyStep.INSTANCE; PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); ClusterService clusterService = mock(ClusterService.class); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); IndexMetaData indexMetaData = IndexMetaData.builder("my_index").settings(settings(Version.CURRENT)) .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); @@ -136,7 +144,7 @@ public void testRunPolicyErrorStep() { PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); ClusterService clusterService = mock(ClusterService.class); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); LifecycleExecutionState.Builder newState = LifecycleExecutionState.builder(); newState.setFailedStep(stepKey.getName()); newState.setIsAutoRetryableError(false); @@ -176,7 +184,7 @@ public void testRunPolicyErrorStepOnRetryableFailedStep() { PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, waitForRolloverStep); ClusterService clusterService = mock(ClusterService.class); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); LifecycleExecutionState.Builder newState = LifecycleExecutionState.builder(); newState.setFailedStep(stepKey.getName()); newState.setIsAutoRetryableError(true); @@ -221,7 +229,7 @@ public void testRunStateChangePolicyWithNoNextStep() throws Exception { .localNodeId(node.getId())) .build(); ClusterServiceUtils.setState(clusterService, state); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); ClusterState before = clusterService.state(); CountDownLatch latch = new CountDownLatch(1); @@ -282,7 +290,8 @@ public void testRunStateChangePolicyWithNextStep() throws Exception { .build(); ClusterServiceUtils.setState(clusterService, state); long stepTime = randomLong(); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> stepTime); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, + clusterService, threadPool, () -> stepTime); ClusterState before = clusterService.state(); CountDownLatch latch = new CountDownLatch(1); @@ -303,6 +312,14 @@ public void testRunStateChangePolicyWithNextStep() throws Exception { assertThat(nextStep.getExecuteCount(), equalTo(1L)); clusterService.close(); threadPool.shutdownNow(); + + ILMHistoryItem historyItem = historyStore.getItems().stream() + .findFirst() + .orElseThrow(() -> new AssertionError("failed to register ILM history")); + assertThat(historyItem.toString(), + containsString("{\"index\":\"test\",\"policy\":\"foo\",\"@timestamp\":" + stepTime + + ",\"success\":true,\"state\":{\"phase\":\"phase\",\"action\":\"action\"," + + "\"step\":\"next_cluster_state_action_step\",\"step_time\":\"" + stepTime + "\"}}")); } public void testRunPeriodicPolicyWithFailureToReadPolicy() throws Exception { @@ -357,7 +374,8 @@ public void doTestRunPolicyWithFailureToReadPolicy(boolean asyncAction, boolean .build(); ClusterServiceUtils.setState(clusterService, state); long stepTime = randomLong(); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> stepTime); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, + clusterService, threadPool, () -> stepTime); ClusterState before = clusterService.state(); if (asyncAction) { @@ -409,7 +427,7 @@ public void testRunAsyncActionDoesNotRun() { .localNodeId(node.getId())) .build(); ClusterServiceUtils.setState(clusterService, state); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); ClusterState before = clusterService.state(); // State changes should not run AsyncAction steps @@ -468,7 +486,7 @@ public void testRunStateChangePolicyWithAsyncActionNextStep() throws Exception { .build(); logger.info("--> state: {}", state); ClusterServiceUtils.setState(clusterService, state); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); ClusterState before = clusterService.state(); CountDownLatch latch = new CountDownLatch(1); @@ -488,6 +506,13 @@ public void testRunStateChangePolicyWithAsyncActionNextStep() throws Exception { assertThat(nextStep.getExecuteCount(), equalTo(1L)); clusterService.close(); threadPool.shutdownNow(); + + ILMHistoryItem historyItem = historyStore.getItems().stream() + .findFirst() + .orElseThrow(() -> new AssertionError("failed to register ILM history")); + assertThat(historyItem.toString(), + containsString("{\"index\":\"test\",\"policy\":\"foo\",\"@timestamp\":0,\"success\":true," + + "\"state\":{\"phase\":\"phase\",\"action\":\"action\",\"step\":\"async_action_step\",\"step_time\":\"0\"}}")); } public void testRunPeriodicStep() throws Exception { @@ -535,7 +560,7 @@ public void testRunPeriodicStep() throws Exception { .build(); logger.info("--> state: {}", state); ClusterServiceUtils.setState(clusterService, state); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); ClusterState before = clusterService.state(); CountDownLatch latch = new CountDownLatch(1); @@ -558,7 +583,7 @@ public void testRunPolicyClusterStateActionStep() { MockClusterStateActionStep step = new MockClusterStateActionStep(stepKey, null); PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); ClusterService clusterService = mock(ClusterService.class); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); IndexMetaData indexMetaData = IndexMetaData.builder("my_index").settings(settings(Version.CURRENT)) .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); @@ -576,7 +601,7 @@ public void testRunPolicyClusterStateWaitStep() { step.setWillComplete(true); PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); ClusterService clusterService = mock(ClusterService.class); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); IndexMetaData indexMetaData = IndexMetaData.builder("my_index").settings(settings(Version.CURRENT)) .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); @@ -595,7 +620,7 @@ public void testRunPolicyAsyncActionStepClusterStateChangeIgnored() { step.setException(expectedException); PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); ClusterService clusterService = mock(ClusterService.class); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); IndexMetaData indexMetaData = IndexMetaData.builder("my_index").settings(settings(Version.CURRENT)) .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); @@ -613,7 +638,7 @@ public void testRunPolicyAsyncWaitStepClusterStateChangeIgnored() { step.setException(expectedException); PolicyStepsRegistry stepRegistry = createOneStepPolicyStepRegistry(policyName, step); ClusterService clusterService = mock(ClusterService.class); - IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, clusterService, threadPool, () -> 0L); + IndexLifecycleRunner runner = new IndexLifecycleRunner(stepRegistry, historyStore, clusterService, threadPool, () -> 0L); IndexMetaData indexMetaData = IndexMetaData.builder("my_index").settings(settings(Version.CURRENT)) .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); @@ -627,7 +652,7 @@ public void testRunPolicyThatDoesntExist() { String policyName = "cluster_state_action_policy"; ClusterService clusterService = mock(ClusterService.class); IndexLifecycleRunner runner = new IndexLifecycleRunner(new PolicyStepsRegistry(NamedXContentRegistry.EMPTY, null), - clusterService, threadPool, () -> 0L); + historyStore, clusterService, threadPool, () -> 0L); IndexMetaData indexMetaData = IndexMetaData.builder("my_index").settings(settings(Version.CURRENT)) .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); // verify that no exception is thrown @@ -712,7 +737,8 @@ public void testIsReadyToTransition() { stepMap, NamedXContentRegistry.EMPTY, null); ClusterService clusterService = mock(ClusterService.class); final AtomicLong now = new AtomicLong(5); - IndexLifecycleRunner runner = new IndexLifecycleRunner(policyStepsRegistry, clusterService, threadPool, now::get); + IndexLifecycleRunner runner = new IndexLifecycleRunner(policyStepsRegistry, historyStore, + clusterService, threadPool, now::get); IndexMetaData indexMetaData = IndexMetaData.builder("my_index").settings(settings(Version.CURRENT)) .numberOfShards(randomIntBetween(1, 5)) .numberOfReplicas(randomIntBetween(0, 5)) @@ -1086,4 +1112,23 @@ public static MockPolicyStepsRegistry createMultiStepPolicyStepRegistry(String p when(client.settings()).thenReturn(Settings.EMPTY); return new MockPolicyStepsRegistry(lifecyclePolicyMap, firstStepMap, stepMap, REGISTRY, client); } + + private class NoOpHistoryStore extends ILMHistoryStore { + + private final List items = new ArrayList<>(); + + NoOpHistoryStore() { + super(Settings.EMPTY, noopClient, null); + } + + public List getItems() { + return items; + } + + @Override + public void putAsync(ILMHistoryItem item) { + logger.info("--> adding ILM history item: [{}]", item); + items.add(item); + } + } } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java index a7f15419d3718..2bf054d318fb5 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java @@ -108,7 +108,7 @@ public void prepareServices() { threadPool = new TestThreadPool("test"); indexLifecycleService = new IndexLifecycleService(Settings.EMPTY, client, clusterService, threadPool, - clock, () -> now, null); + clock, () -> now, null, null); Mockito.verify(clusterService).addListener(indexLifecycleService); Mockito.verify(clusterService).addStateApplier(indexLifecycleService); } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java index fa2a626b0f9bc..39b3fca46c1f1 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java @@ -75,7 +75,7 @@ public void testExecuteSuccessfullyMoved() throws IOException { setStateToKey(currentStepKey); MoveToErrorStepUpdateTask task = new MoveToErrorStepUpdateTask(index, policy, currentStepKey, cause, () -> now, - (idxMeta, stepKey) -> new MockStep(stepKey, nextStepKey)); + (idxMeta, stepKey) -> new MockStep(stepKey, nextStepKey), state -> {}); ClusterState newState = task.execute(clusterState); LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.getMetaData().index(index)); StepKey actualKey = LifecycleExecutionState.getCurrentStepKey(lifecycleState); @@ -101,7 +101,7 @@ public void testExecuteNoopDifferentStep() throws IOException { Exception cause = new ElasticsearchException("THIS IS AN EXPECTED CAUSE"); setStateToKey(notCurrentStepKey); MoveToErrorStepUpdateTask task = new MoveToErrorStepUpdateTask(index, policy, currentStepKey, cause, () -> now, - (idxMeta, stepKey) -> new MockStep(stepKey, new StepKey("next-phase", "action", "step"))); + (idxMeta, stepKey) -> new MockStep(stepKey, new StepKey("next-phase", "action", "step")), state -> {}); ClusterState newState = task.execute(clusterState); assertThat(newState, sameInstance(clusterState)); } @@ -113,7 +113,7 @@ public void testExecuteNoopDifferentPolicy() throws IOException { setStateToKey(currentStepKey); setStatePolicy("not-" + policy); MoveToErrorStepUpdateTask task = new MoveToErrorStepUpdateTask(index, policy, currentStepKey, cause, () -> now, - (idxMeta, stepKey) -> new MockStep(stepKey, new StepKey("next-phase", "action", "step"))); + (idxMeta, stepKey) -> new MockStep(stepKey, new StepKey("next-phase", "action", "step")), state -> {}); ClusterState newState = task.execute(clusterState); assertThat(newState, sameInstance(clusterState)); } @@ -126,7 +126,7 @@ public void testOnFailure() { setStateToKey(currentStepKey); MoveToErrorStepUpdateTask task = new MoveToErrorStepUpdateTask(index, policy, currentStepKey, cause, () -> now, - (idxMeta, stepKey) -> new MockStep(stepKey, new StepKey("next-phase", "action", "step"))); + (idxMeta, stepKey) -> new MockStep(stepKey, new StepKey("next-phase", "action", "step")), state -> {}); Exception expectedException = new RuntimeException(); ElasticsearchException exception = expectThrows(ElasticsearchException.class, () -> task.onFailure(randomAlphaOfLength(10), expectedException)); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItemTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItemTests.java new file mode 100644 index 0000000000000..2e3f76c6370e7 --- /dev/null +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItemTests.java @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.ilm.history; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; + +import java.io.IOException; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; + +public class ILMHistoryItemTests extends ESTestCase { + + public void testToXContent() throws IOException { + ILMHistoryItem success = ILMHistoryItem.success("index", "policy", 1234L, 100L, + LifecycleExecutionState.builder() + .setPhase("phase") + .setAction("action") + .setStep("step") + .setPhaseTime(10L) + .setActionTime(20L) + .setStepTime(30L) + .setPhaseDefinition("{}") + .setStepInfo("{\"step_info\": \"foo\"") + .build()); + + ILMHistoryItem failure = ILMHistoryItem.failure("index", "policy", 1234L, 100L, + LifecycleExecutionState.builder() + .setPhase("phase") + .setAction("action") + .setStep("ERROR") + .setFailedStep("step") + .setFailedStepRetryCount(7) + .setIsAutoRetryableError(true) + .setPhaseTime(10L) + .setActionTime(20L) + .setStepTime(30L) + .setPhaseDefinition("{\"phase_json\": \"eggplant\"}") + .setStepInfo("{\"step_info\": \"foo\"") + .build(), + new IllegalArgumentException("failure")); + + try (XContentBuilder builder = jsonBuilder()) { + success.toXContent(builder, ToXContent.EMPTY_PARAMS); + String json = Strings.toString(builder); + assertThat(json, equalTo("{\"index\":\"index\"," + + "\"policy\":\"policy\"," + + "\"@timestamp\":1234," + + "\"index_age\":100," + + "\"success\":true," + + "\"state\":{\"phase\":\"phase\"," + + "\"phase_definition\":\"{}\"," + + "\"action_time\":\"20\"," + + "\"phase_time\":\"10\"," + + "\"step_info\":\"{\\\"step_info\\\": \\\"foo\\\"\",\"action\":\"action\",\"step\":\"step\",\"step_time\":\"30\"}}" + )); + } + + try (XContentBuilder builder = jsonBuilder()) { + failure.toXContent(builder, ToXContent.EMPTY_PARAMS); + String json = Strings.toString(builder); + assertThat(json, startsWith("{\"index\":\"index\"," + + "\"policy\":\"policy\"," + + "\"@timestamp\":1234," + + "\"index_age\":100," + + "\"success\":false," + + "\"state\":{\"phase\":\"phase\"," + + "\"failed_step\":\"step\"," + + "\"phase_definition\":\"{\\\"phase_json\\\": \\\"eggplant\\\"}\"," + + "\"action_time\":\"20\"," + + "\"is_auto_retryable_error\":\"true\"," + + "\"failed_step_retry_count\":\"7\"," + + "\"phase_time\":\"10\"," + + "\"step_info\":\"{\\\"step_info\\\": \\\"foo\\\"\"," + + "\"action\":\"action\"," + + "\"step\":\"ERROR\"," + + "\"step_time\":\"30\"}," + + "\"error_details\":\"{\\\"type\\\":\\\"illegal_argument_exception\\\"," + + "\\\"reason\\\":\\\"failure\\\"," + + "\\\"stack_trace\\\":\\\"java.lang.IllegalArgumentException: failure")); + } + } +} diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStoreTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStoreTests.java new file mode 100644 index 0000000000000..3e6b9a638737c --- /dev/null +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStoreTests.java @@ -0,0 +1,398 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.ilm.history; + +import org.elasticsearch.ResourceAlreadyExistsException; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.LatchedActionListener; +import org.elasticsearch.action.admin.indices.create.CreateIndexAction; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; +import org.elasticsearch.action.bulk.BulkAction; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.AliasMetaData; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.TriFunction; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.test.ClusterServiceUtils; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.client.NoOpClient; +import org.elasticsearch.test.hamcrest.ElasticsearchAssertions; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; + +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import static org.elasticsearch.xpack.core.ilm.LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING; +import static org.elasticsearch.xpack.ilm.history.ILMHistoryStore.ILM_HISTORY_ALIAS; +import static org.elasticsearch.xpack.ilm.history.ILMHistoryStore.ILM_HISTORY_INDEX_PREFIX; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +public class ILMHistoryStoreTests extends ESTestCase { + + private ThreadPool threadPool; + private VerifyingClient client; + private ClusterService clusterService; + private ILMHistoryStore historyStore; + + @Before + public void setup() { + threadPool = new TestThreadPool(this.getClass().getName()); + client = new VerifyingClient(threadPool); + clusterService = ClusterServiceUtils.createClusterService(threadPool); + historyStore = new ILMHistoryStore(Settings.EMPTY, client, clusterService); + } + + @After + public void setdown() { + historyStore.close(); + clusterService.close(); + client.close(); + threadPool.shutdownNow(); + } + + public void testNoActionIfDisabled() throws Exception { + Settings settings = Settings.builder().put(LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING.getKey(), false).build(); + try (ILMHistoryStore disabledHistoryStore = new ILMHistoryStore(settings, client, null)) { + String policyId = randomAlphaOfLength(5); + final long timestamp = randomNonNegativeLong(); + ILMHistoryItem record = ILMHistoryItem.success("index", policyId, timestamp, null, null); + + CountDownLatch latch = new CountDownLatch(1); + client.setVerifier((a, r, l) -> { + fail("the history store is disabled, no action should have been taken"); + latch.countDown(); + return null; + }); + disabledHistoryStore.putAsync(record); + latch.await(10, TimeUnit.SECONDS); + } + } + + @SuppressWarnings("unchecked") + public void testPut() throws Exception { + String policyId = randomAlphaOfLength(5); + final long timestamp = randomNonNegativeLong(); + { + ILMHistoryItem record = ILMHistoryItem.success("index", policyId, timestamp, 10L, + LifecycleExecutionState.builder() + .setPhase("phase") + .build()); + + AtomicInteger calledTimes = new AtomicInteger(0); + client.setVerifier((action, request, listener) -> { + if (action instanceof CreateIndexAction && request instanceof CreateIndexRequest) { + return new CreateIndexResponse(true, true, ((CreateIndexRequest) request).index()); + } + calledTimes.incrementAndGet(); + assertThat(action, instanceOf(BulkAction.class)); + assertThat(request, instanceOf(BulkRequest.class)); + BulkRequest bulkRequest = (BulkRequest) request; + bulkRequest.requests().forEach(dwr -> assertEquals(ILM_HISTORY_ALIAS, dwr.index())); + assertNotNull(listener); + + // The content of this BulkResponse doesn't matter, so just make it have the same number of responses + int responses = bulkRequest.numberOfActions(); + return new BulkResponse(IntStream.range(0, responses) + .mapToObj(i -> new BulkItemResponse(i, DocWriteRequest.OpType.INDEX, + new IndexResponse(new ShardId("index", "uuid", 0), randomAlphaOfLength(10), 1, 1, 1, true))) + .toArray(BulkItemResponse[]::new), + 1000L); + }); + + historyStore.putAsync(record); + assertBusy(() -> assertThat(calledTimes.get(), equalTo(1))); + } + + { + final String cause = randomAlphaOfLength(9); + Exception failureException = new RuntimeException(cause); + ILMHistoryItem record = ILMHistoryItem.failure("index", policyId, timestamp, 10L, + LifecycleExecutionState.builder() + .setPhase("phase") + .build(), failureException); + + AtomicInteger calledTimes = new AtomicInteger(0); + client.setVerifier((action, request, listener) -> { + if (action instanceof CreateIndexAction && request instanceof CreateIndexRequest) { + return new CreateIndexResponse(true, true, ((CreateIndexRequest) request).index()); + } + calledTimes.incrementAndGet(); + assertThat(action, instanceOf(BulkAction.class)); + assertThat(request, instanceOf(BulkRequest.class)); + BulkRequest bulkRequest = (BulkRequest) request; + bulkRequest.requests().forEach(dwr -> { + assertEquals(ILM_HISTORY_ALIAS, dwr.index()); + assertThat(dwr, instanceOf(IndexRequest.class)); + IndexRequest ir = (IndexRequest) dwr; + String indexedDocument = ir.source().utf8ToString(); + assertThat(indexedDocument, Matchers.containsString("runtime_exception")); + assertThat(indexedDocument, Matchers.containsString(cause)); + }); + assertNotNull(listener); + + // The content of this BulkResponse doesn't matter, so just make it have the same number of responses with failures + int responses = bulkRequest.numberOfActions(); + return new BulkResponse(IntStream.range(0, responses) + .mapToObj(i -> new BulkItemResponse(i, DocWriteRequest.OpType.INDEX, + new BulkItemResponse.Failure("index", i + "", failureException))) + .toArray(BulkItemResponse[]::new), + 1000L); + }); + + historyStore.putAsync(record); + assertBusy(() -> assertThat(calledTimes.get(), equalTo(1))); + } + } + + public void testHistoryIndexNeedsCreation() throws InterruptedException { + ClusterState state = ClusterState.builder(new ClusterName(randomAlphaOfLength(5))) + .metaData(MetaData.builder()) + .build(); + + client.setVerifier((a, r, l) -> { + assertThat(a, instanceOf(CreateIndexAction.class)); + assertThat(r, instanceOf(CreateIndexRequest.class)); + CreateIndexRequest request = (CreateIndexRequest) r; + assertThat(request.aliases(), Matchers.hasSize(1)); + request.aliases().forEach(alias -> { + assertThat(alias.name(), equalTo(ILM_HISTORY_ALIAS)); + assertTrue(alias.writeIndex()); + }); + return new CreateIndexResponse(true, true, request.index()); + }); + + CountDownLatch latch = new CountDownLatch(1); + ILMHistoryStore.ensureHistoryIndex(client, state, new LatchedActionListener<>(ActionListener.wrap( + Assert::assertTrue, + ex -> { + logger.error(ex); + fail("should have called onResponse, not onFailure"); + }), latch)); + + ElasticsearchAssertions.awaitLatch(latch, 10, TimeUnit.SECONDS); + } + + public void testHistoryIndexProperlyExistsAlready() throws InterruptedException { + ClusterState state = ClusterState.builder(new ClusterName(randomAlphaOfLength(5))) + .metaData(MetaData.builder() + .put(IndexMetaData.builder(ILM_HISTORY_INDEX_PREFIX + "000001") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(randomIntBetween(1,10)) + .numberOfReplicas(randomIntBetween(1,10)) + .putAlias(AliasMetaData.builder(ILM_HISTORY_ALIAS) + .writeIndex(true) + .build()))) + .build(); + + client.setVerifier((a, r, l) -> { + fail("no client calls should have been made"); + return null; + }); + + CountDownLatch latch = new CountDownLatch(1); + ILMHistoryStore.ensureHistoryIndex(client, state, new LatchedActionListener<>(ActionListener.wrap( + Assert::assertFalse, + ex -> { + logger.error(ex); + fail("should have called onResponse, not onFailure"); + }), latch)); + + ElasticsearchAssertions.awaitLatch(latch, 10, TimeUnit.SECONDS); + } + + public void testHistoryIndexHasNoWriteIndex() throws InterruptedException { + ClusterState state = ClusterState.builder(new ClusterName(randomAlphaOfLength(5))) + .metaData(MetaData.builder() + .put(IndexMetaData.builder(ILM_HISTORY_INDEX_PREFIX + "000001") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(randomIntBetween(1,10)) + .numberOfReplicas(randomIntBetween(1,10)) + .putAlias(AliasMetaData.builder(ILM_HISTORY_ALIAS) + .build())) + .put(IndexMetaData.builder(randomAlphaOfLength(5)) + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(randomIntBetween(1,10)) + .numberOfReplicas(randomIntBetween(1,10)) + .putAlias(AliasMetaData.builder(ILM_HISTORY_ALIAS) + .build()))) + .build(); + + client.setVerifier((a, r, l) -> { + fail("no client calls should have been made"); + return null; + }); + + CountDownLatch latch = new CountDownLatch(1); + ILMHistoryStore.ensureHistoryIndex(client, state, new LatchedActionListener<>(ActionListener.wrap( + indexCreated -> fail("should have called onFailure, not onResponse"), + ex -> { + assertThat(ex, instanceOf(IllegalStateException.class)); + assertThat(ex.getMessage(), Matchers.containsString("ILM history alias [" + ILM_HISTORY_ALIAS + + "does not have a write index")); + }), latch)); + + ElasticsearchAssertions.awaitLatch(latch, 10, TimeUnit.SECONDS); + } + + public void testHistoryIndexNotAlias() throws InterruptedException { + ClusterState state = ClusterState.builder(new ClusterName(randomAlphaOfLength(5))) + .metaData(MetaData.builder() + .put(IndexMetaData.builder(ILM_HISTORY_ALIAS) + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(randomIntBetween(1,10)) + .numberOfReplicas(randomIntBetween(1,10)))) + .build(); + + client.setVerifier((a, r, l) -> { + fail("no client calls should have been made"); + return null; + }); + + CountDownLatch latch = new CountDownLatch(1); + ILMHistoryStore.ensureHistoryIndex(client, state, new LatchedActionListener<>(ActionListener.wrap( + indexCreated -> fail("should have called onFailure, not onResponse"), + ex -> { + assertThat(ex, instanceOf(IllegalStateException.class)); + assertThat(ex.getMessage(), Matchers.containsString("ILM history alias [" + ILM_HISTORY_ALIAS + + "] already exists as concrete index")); + }), latch)); + + ElasticsearchAssertions.awaitLatch(latch, 10, TimeUnit.SECONDS); + } + + public void testHistoryIndexCreatedConcurrently() throws InterruptedException { + ClusterState state = ClusterState.builder(new ClusterName(randomAlphaOfLength(5))) + .metaData(MetaData.builder()) + .build(); + + client.setVerifier((a, r, l) -> { + assertThat(a, instanceOf(CreateIndexAction.class)); + assertThat(r, instanceOf(CreateIndexRequest.class)); + CreateIndexRequest request = (CreateIndexRequest) r; + assertThat(request.aliases(), Matchers.hasSize(1)); + request.aliases().forEach(alias -> { + assertThat(alias.name(), equalTo(ILM_HISTORY_ALIAS)); + assertTrue(alias.writeIndex()); + }); + throw new ResourceAlreadyExistsException("that index already exists"); + }); + + CountDownLatch latch = new CountDownLatch(1); + ILMHistoryStore.ensureHistoryIndex(client, state, new LatchedActionListener<>(ActionListener.wrap( + Assert::assertFalse, + ex -> { + logger.error(ex); + fail("should have called onResponse, not onFailure"); + }), latch)); + + ElasticsearchAssertions.awaitLatch(latch, 10, TimeUnit.SECONDS); + } + + public void testHistoryAliasDoesntExistButIndexDoes() throws InterruptedException { + final String initialIndex = ILM_HISTORY_INDEX_PREFIX + "000001"; + ClusterState state = ClusterState.builder(new ClusterName(randomAlphaOfLength(5))) + .metaData(MetaData.builder() + .put(IndexMetaData.builder(initialIndex) + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(randomIntBetween(1,10)) + .numberOfReplicas(randomIntBetween(1,10)))) + .build(); + + client.setVerifier((a, r, l) -> { + fail("no client calls should have been made"); + return null; + }); + + CountDownLatch latch = new CountDownLatch(1); + ILMHistoryStore.ensureHistoryIndex(client, state, new LatchedActionListener<>(ActionListener.wrap( + response -> { + logger.error(response); + fail("should have called onFailure, not onResponse"); + }, + ex -> { + assertThat(ex, instanceOf(IllegalStateException.class)); + assertThat(ex.getMessage(), Matchers.containsString("ILM history index [" + initialIndex + + "] already exists but does not have alias [" + ILM_HISTORY_ALIAS + "]")); + }), latch)); + + ElasticsearchAssertions.awaitLatch(latch, 10, TimeUnit.SECONDS); + } + + @SuppressWarnings("unchecked") + private void assertContainsMap(String indexedDocument, Map map) { + map.forEach((k, v) -> { + assertThat(indexedDocument, Matchers.containsString(k)); + if (v instanceof Map) { + assertContainsMap(indexedDocument, (Map) v); + } + if (v instanceof Iterable) { + ((Iterable) v).forEach(elem -> { + assertThat(indexedDocument, Matchers.containsString(elem.toString())); + }); + } else { + assertThat(indexedDocument, Matchers.containsString(v.toString())); + } + }); + } + + /** + * A client that delegates to a verifying function for action/request/listener + */ + public static class VerifyingClient extends NoOpClient { + + private TriFunction, ActionRequest, ActionListener, ActionResponse> verifier = (a, r, l) -> { + fail("verifier not set"); + return null; + }; + + VerifyingClient(ThreadPool threadPool) { + super(threadPool); + } + + @Override + @SuppressWarnings("unchecked") + protected void doExecute(ActionType action, + Request request, + ActionListener listener) { + try { + listener.onResponse((Response) verifier.apply(action, request, listener)); + } catch (Exception e) { + listener.onFailure(e); + } + } + + VerifyingClient setVerifier(TriFunction, ActionRequest, ActionListener, ActionResponse> verifier) { + this.verifier = verifier; + return this; + } + } +} diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/test/MonitoringIntegTestCase.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/test/MonitoringIntegTestCase.java index 22f3b59220998..8270f97d7c01b 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/test/MonitoringIntegTestCase.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/test/MonitoringIntegTestCase.java @@ -54,6 +54,7 @@ protected Settings nodeSettings(int nodeOrdinal) { // .put(MachineLearningField.AUTODETECT_PROCESS.getKey(), false) // .put(XPackSettings.MACHINE_LEARNING_ENABLED.getKey(), false) // we do this by default in core, but for monitoring this isn't needed and only adds noise. + .put("index.lifecycle.history_index_enabled", false) .put("index.store.mock.check_index_on_close", false); return builder.build(); diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/AbstractWatcherIntegrationTestCase.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/AbstractWatcherIntegrationTestCase.java index 82a9cffc35a9d..0c1377ac79767 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/AbstractWatcherIntegrationTestCase.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/AbstractWatcherIntegrationTestCase.java @@ -116,6 +116,7 @@ protected Settings nodeSettings(int nodeOrdinal) { // watcher settings that should work despite randomization .put("xpack.watcher.execution.scroll.size", randomIntBetween(1, 100)) .put("xpack.watcher.watch.scroll.size", randomIntBetween(1, 100)) + .put("index.lifecycle.history_index_enabled", false) .build(); } From 4e68c855e55c3dbff2f4923aa560d171878450fc Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Wed, 18 Dec 2019 16:19:28 -0700 Subject: [PATCH 264/686] [TEST] Unknown scripting annotations raise error (#50343) Ensure that unknown annotations, such as typo'd `@nondeterministic`, will raise an exception. --- .../painless/WhitelistLoaderTests.java | 20 +++++++++++++++++++ ....elasticsearch.painless.annotation.unknown | 6 ++++++ ...h.painless.annotation.unknown_with_options | 5 +++++ 3 files changed, 31 insertions(+) create mode 100644 modules/lang-painless/src/test/resources/org/elasticsearch/painless/spi/org.elasticsearch.painless.annotation.unknown create mode 100644 modules/lang-painless/src/test/resources/org/elasticsearch/painless/spi/org.elasticsearch.painless.annotation.unknown_with_options diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhitelistLoaderTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhitelistLoaderTests.java index 7fe9dcce3bc09..59018e60c5db4 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhitelistLoaderTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhitelistLoaderTests.java @@ -31,6 +31,26 @@ import java.util.Map; public class WhitelistLoaderTests extends ScriptTestCase { + public void testUnknownAnnotations() { + Map parsers = new HashMap<>(WhitelistAnnotationParser.BASE_ANNOTATION_PARSERS); + + RuntimeException expected = expectThrows(RuntimeException.class, () -> { + WhitelistLoader.loadFromResourceFiles(Whitelist.class, parsers, "org.elasticsearch.painless.annotation.unknown"); + }); + assertEquals( + "invalid annotation: parser not found for [unknownAnnotation] [@unknownAnnotation]", expected.getCause().getMessage() + ); + assertEquals(IllegalArgumentException.class, expected.getCause().getClass()); + + expected = expectThrows(RuntimeException.class, () -> { + WhitelistLoader.loadFromResourceFiles(Whitelist.class, parsers, "org.elasticsearch.painless.annotation.unknown_with_options"); + }); + assertEquals( + "invalid annotation: parser not found for [unknownAnootationWithMessage] [@unknownAnootationWithMessage[arg=\"arg value\"]]", + expected.getCause().getMessage() + ); + assertEquals(IllegalArgumentException.class, expected.getCause().getClass()); + } public void testAnnotations() { Map parsers = new HashMap<>(WhitelistAnnotationParser.BASE_ANNOTATION_PARSERS); diff --git a/modules/lang-painless/src/test/resources/org/elasticsearch/painless/spi/org.elasticsearch.painless.annotation.unknown b/modules/lang-painless/src/test/resources/org/elasticsearch/painless/spi/org.elasticsearch.painless.annotation.unknown new file mode 100644 index 0000000000000..c67f535705b7a --- /dev/null +++ b/modules/lang-painless/src/test/resources/org/elasticsearch/painless/spi/org.elasticsearch.painless.annotation.unknown @@ -0,0 +1,6 @@ +# whitelist for annotation tests with unknown annotation + +class org.elasticsearch.painless.AnnotationTestObject @no_import { + void unknownAnnotationMethod() @unknownAnnotation + void unknownAnnotationMethod() @unknownAnootationWithMessage[message="use another method"] +} diff --git a/modules/lang-painless/src/test/resources/org/elasticsearch/painless/spi/org.elasticsearch.painless.annotation.unknown_with_options b/modules/lang-painless/src/test/resources/org/elasticsearch/painless/spi/org.elasticsearch.painless.annotation.unknown_with_options new file mode 100644 index 0000000000000..071b7f57fad2d --- /dev/null +++ b/modules/lang-painless/src/test/resources/org/elasticsearch/painless/spi/org.elasticsearch.painless.annotation.unknown_with_options @@ -0,0 +1,5 @@ +# whitelist for annotation tests with unknown annotation containing options + +class org.elasticsearch.painless.AnnotationTestObject @no_import { + void unknownAnnotationMethod() @unknownAnootationWithMessage[arg="arg value"] +} From 899d9eb31f02f479f0f859f911c693a276af7f93 Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Wed, 18 Dec 2019 23:18:11 -0800 Subject: [PATCH 265/686] Review comments by dakrone --- .../xpack/core/ilm/ForceMergeAction.java | 68 +++++++++++-------- .../xpack/core/ilm/WaitForIndexColorStep.java | 43 ++++++------ .../xpack/core/ilm/ForceMergeActionTests.java | 19 ++++-- .../ilm/TimeseriesLifecycleTypeTests.java | 2 +- 4 files changed, 76 insertions(+), 56 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index 78417857a7363..6ee7c3eb2df19 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -10,6 +10,7 @@ import org.elasticsearch.client.Client; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -23,7 +24,7 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -38,13 +39,13 @@ public class ForceMergeAction implements LifecycleAction { private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, a -> { int maxNumSegments = (int) a[0]; - Codec codec = a[1] != null ? Codec.forName((String) a[1]) : Codec.getDefault(); + Codec codec = a[1] != null ? Codec.forName((String) a[1]) : null; return new ForceMergeAction(maxNumSegments, codec); }); static { PARSER.declareInt(ConstructingObjectParser.constructorArg(), MAX_NUM_SEGMENTS_FIELD); - PARSER.declareInt(ConstructingObjectParser.constructorArg(), CODEC); + PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), CODEC); } private final int maxNumSegments; @@ -54,21 +55,24 @@ public static ForceMergeAction parse(XContentParser parser) { return PARSER.apply(parser, null); } - public ForceMergeAction(int maxNumSegments, Codec codec) { + public ForceMergeAction(int maxNumSegments, @Nullable Codec codec) { if (maxNumSegments <= 0) { throw new IllegalArgumentException("[" + MAX_NUM_SEGMENTS_FIELD.getPreferredName() + "] must be a positive integer"); } this.maxNumSegments = maxNumSegments; + if (codec != null && Codec.forName(codec.getName()) == null) { + throw new IllegalArgumentException("Compression type of " + codec.getName() + "does not exist"); + } this.codec = codec; } public ForceMergeAction(StreamInput in) throws IOException { this.maxNumSegments = in.readVInt(); if (in.getVersion().onOrAfter(Version.V_8_0_0)) { - this.codec = Codec.forName(in.readString()); + this.codec = Codec.forName(in.readOptionalString()); } else { - this.codec = Codec.getDefault(); + this.codec = null; } } @@ -84,9 +88,9 @@ public Codec getCodec() { public void writeTo(StreamOutput out) throws IOException { out.writeVInt(maxNumSegments); if (out.getVersion().onOrAfter(Version.V_8_0_0)) { - out.writeString(codec.getName()); + out.writeOptionalString(codec.getName()); } else { - out.writeString(Codec.getDefault().getName()); + out.writeString(null); } } @@ -104,7 +108,9 @@ public boolean isSafeAction() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(MAX_NUM_SEGMENTS_FIELD.getPreferredName(), maxNumSegments); - builder.field(CODEC.getPreferredName(), codec); + if (codec != null) { + builder.field(CODEC.getPreferredName(), codec); + } builder.endObject(); return builder; } @@ -119,28 +125,34 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) StepKey forceMergeKey = new StepKey(phase, NAME, ForceMergeStep.NAME); StepKey countKey = new StepKey(phase, NAME, SegmentCountStep.NAME); - - if (codec.getName().equals(CodecService.BEST_COMPRESSION_CODEC)) { - StepKey closeKey = new StepKey(phase, NAME, CloseIndexStep.NAME); - StepKey openKey = new StepKey(phase, NAME, OpenIndexStep.NAME); - StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexColorStep.NAME); - StepKey updateCompressionKey = new StepKey(phase, NAME, UpdateSettingsStep.NAME); - - CloseIndexStep closeIndexStep = new CloseIndexStep(closeKey, updateCompressionKey, client); - UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey, - openKey, client, bestCompressionSettings); - OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client); - WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey, - forceMergeKey, ClusterHealthStatus.GREEN); - ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, nextStepKey, client, maxNumSegments); - return Arrays.asList(closeIndexStep, updateBestCompressionSettings, - openIndexStep, waitForIndexGreenStep, forceMergeStep); - } + StepKey closeKey = new StepKey(phase, NAME, CloseIndexStep.NAME); + StepKey openKey = new StepKey(phase, NAME, OpenIndexStep.NAME); + StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexColorStep.NAME); + StepKey updateCompressionKey = new StepKey(phase, NAME, UpdateSettingsStep.NAME); UpdateSettingsStep readOnlyStep = new UpdateSettingsStep(readOnlyKey, forceMergeKey, client, readOnlySettings); ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, countKey, client, maxNumSegments); SegmentCountStep segmentCountStep = new SegmentCountStep(countKey, nextStepKey, client, maxNumSegments); - return Arrays.asList(readOnlyStep, forceMergeStep, segmentCountStep); + CloseIndexStep closeIndexStep = new CloseIndexStep(closeKey, updateCompressionKey, client); + UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey, + openKey, client, bestCompressionSettings); + OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client); + WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey, + forceMergeKey, ClusterHealthStatus.GREEN); + + List mergeSteps = new ArrayList<>(); + mergeSteps.add(readOnlyStep); + + if (codec.getName().equals(CodecService.BEST_COMPRESSION_CODEC)) { + mergeSteps.add(closeIndexStep); + mergeSteps.add(updateBestCompressionSettings); + mergeSteps.add(openIndexStep); + mergeSteps.add(waitForIndexGreenStep); + } + + mergeSteps.add(forceMergeStep); + mergeSteps.add(segmentCountStep); + return mergeSteps; } @Override @@ -158,7 +170,7 @@ public boolean equals(Object obj) { } ForceMergeAction other = (ForceMergeAction) obj; return Objects.equals(maxNumSegments, other.maxNumSegments) - && Objects.equals(codec, other.codec); + && ((codec == null && other.codec == null) || Objects.equals(codec, other.codec)); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java index 812ad66323668..bfaf6ab5bfcb1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java @@ -23,7 +23,7 @@ /** * Wait Step for index based on color - * */ + */ class WaitForIndexColorStep extends ClusterStateWaitStep { @@ -47,8 +47,12 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (obj == null) return false; - if (!(obj instanceof WaitForIndexColorStep)) return false; + if (obj == null) { + return false; + } + if (obj.getClass() != getClass()) { + return false; + } WaitForIndexColorStep other = (WaitForIndexColorStep) obj; return super.equals(obj) && Objects.equals(this.color, other.color); } @@ -69,7 +73,7 @@ public Result isConditionMet(Index index, ClusterState clusterState) { result = waitForRed(indexRoutingTable); break; default: - result = new Result(false, new Info("No index color match")); + result = new Result(false, new Info("no index color match")); break; } return result; @@ -77,14 +81,14 @@ public Result isConditionMet(Index index, ClusterState clusterState) { private Result waitForRed(IndexRoutingTable indexRoutingTable) { if (indexRoutingTable == null) { - return new Result(true, new Info("Index is red")); + return new Result(true, new Info("index is red")); } - return new Result(false, new Info("Index is not red")); + return new Result(false, new Info("index is not red")); } private Result waitForYellow(IndexRoutingTable indexRoutingTable) { if (indexRoutingTable == null) { - return new Result(false, new Info("index is red; no IndexRoutingTable")); + return new Result(false, new Info("index is red; no indexRoutingTable")); } boolean indexIsAtLeastYellow = indexRoutingTable.allPrimaryShardsActive(); @@ -97,27 +101,20 @@ private Result waitForYellow(IndexRoutingTable indexRoutingTable) { private Result waitForGreen(IndexRoutingTable indexRoutingTable) { if (indexRoutingTable == null) { - return new Result(false, new Info("index is red; no IndexRoutingTable")); + return new Result(false, new Info("index is red; no indexRoutingTable")); } - boolean indexIsGreen = false; if (indexRoutingTable.allPrimaryShardsActive()) { - boolean replicaIndexIsGreen = false; for (ObjectCursor shardRouting : indexRoutingTable.getShards().values()) { - replicaIndexIsGreen = shardRouting.value.replicaShards().stream().allMatch(ShardRouting::active); - if (!replicaIndexIsGreen) { + boolean replicaIndexIsGreen = shardRouting.value.replicaShards().stream().allMatch(ShardRouting::active); + if (replicaIndexIsGreen == false) { return new Result(false, new Info("index is yellow; not all replica shards are active")); } } - indexIsGreen = replicaIndexIsGreen; - } - - - if (indexIsGreen) { return new Result(true, null); - } else { - return new Result(false, new Info("index is not green; not all shards are active")); } + + return new Result(false, new Info("index is not green; not all shards are active")); } static final class Info implements ToXContentObject { @@ -144,8 +141,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (o == null) { + return false; + } + if (getClass() != o.getClass()) { + return false; + } Info info = (Info) o; return Objects.equals(getMessage(), info.getMessage()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index ef88e461c0d13..042c12b9fcb78 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -41,7 +41,7 @@ static ForceMergeAction randomInstance() { static Codec createRandomCompressionSettings() { if(randomBoolean()) { - return Codec.getDefault(); + return null; } return Codec.forName(CodecService.BEST_COMPRESSION_CODEC); } @@ -50,7 +50,7 @@ static Codec createRandomCompressionSettings() { protected ForceMergeAction mutateInstance(ForceMergeAction instance) { int maxNumSegments = instance.getMaxNumSegments(); maxNumSegments = maxNumSegments + randomIntBetween(1, 10); - return new ForceMergeAction(maxNumSegments, Codec.getDefault()); + return new ForceMergeAction(maxNumSegments, createRandomCompressionSettings()); } @Override @@ -81,12 +81,13 @@ private void assertBestCompression(ForceMergeAction instance) { StepKey nextStepKey = new StepKey(randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10)); List steps = instance.toSteps(null, phase, nextStepKey); assertNotNull(steps); - assertEquals(5, steps.size()); + assertEquals(6, steps.size()); CloseIndexStep firstStep = (CloseIndexStep) steps.get(0); UpdateSettingsStep secondStep = (UpdateSettingsStep) steps.get(1); OpenIndexStep thirdStep = (OpenIndexStep) steps.get(2); WaitForIndexColorStep fourthStep = (WaitForIndexColorStep) steps.get(3); ForceMergeStep fifthStep = (ForceMergeStep) steps.get(4); + SegmentCountStep sixthStep = (SegmentCountStep) steps.get(4); assertThat(firstStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, CloseIndexStep.NAME))); assertThat(firstStep.getNextStepKey(), equalTo(secondStep.getKey())); assertThat(secondStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, UpdateSettingsStep.NAME))); @@ -98,6 +99,8 @@ private void assertBestCompression(ForceMergeAction instance) { assertThat(fourthStep.getNextStepKey(), equalTo(fifthStep)); assertThat(fifthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, ForceMergeStep.NAME))); assertThat(fifthStep.getNextStepKey(), equalTo(nextStepKey)); + assertThat(sixthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, SegmentCountStep.NAME))); + assertThat(sixthStep.getNextStepKey(), equalTo(nextStepKey)); } public void testMissingMaxNumSegments() throws IOException { @@ -105,18 +108,22 @@ public void testMissingMaxNumSegments() throws IOException { XContentParser parser = XContentHelper.createParser(null, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, emptyObject, XContentType.JSON); Exception e = expectThrows(IllegalArgumentException.class, () -> ForceMergeAction.parse(parser)); - assertThat(e.getMessage(), equalTo("Required [max_num_segments, best_compression]")); + assertThat(e.getMessage(), equalTo("Required [max_num_segments]")); } public void testInvalidNegativeSegmentNumber() { Exception r = expectThrows(IllegalArgumentException.class, () -> new - ForceMergeAction(randomIntBetween(-10, 0), Codec.getDefault())); + ForceMergeAction(randomIntBetween(-10, 0), null)); assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer")); } + public void testInvalidCodec() { + + } + public void testToSteps() { ForceMergeAction instance = createTestInstance(); - if (CodecService.BEST_COMPRESSION_CODEC.equals(instance.getCodec().getName())) { + if (instance.getCodec() != null && CodecService.BEST_COMPRESSION_CODEC.equals(instance.getCodec().getName())) { assertBestCompression(instance); } else { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java index 1469e40e6682d..e1df27f09a44f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java @@ -493,7 +493,7 @@ private ConcurrentMap convertActionNamesToActions(Strin case DeleteAction.NAME: return new DeleteAction(); case ForceMergeAction.NAME: - return new ForceMergeAction(1, Codec.getDefault()); + return new ForceMergeAction(1, null); case ReadOnlyAction.NAME: return new ReadOnlyAction(); case RolloverAction.NAME: From 5b4149fc0b0fdf6427b1f8df34e42a4cba9ec1b0 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Thu, 19 Dec 2019 09:45:44 +0000 Subject: [PATCH 266/686] Mute TimeSeriesLifecycleActionsIT. testHistoryIsWrittenWithFailure Relates to #50353 --- .../elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 29976664bc896..7192febb6b11e 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -1047,6 +1047,7 @@ public void testHistoryIsWrittenWithSuccess() throws Exception { assertBusy(() -> assertHistoryIsPresent(policy, index + "-000002", true, "check-rollover-ready"), 30, TimeUnit.SECONDS); } + @AwaitsFix( bugUrl = "https://github.com/elastic/elasticsearch/issues/50353") public void testHistoryIsWrittenWithFailure() throws Exception { String index = "index"; From 0ef7bbe0a860909e2b5d56d62ffc2de4467e34ef Mon Sep 17 00:00:00 2001 From: David Kyle Date: Thu, 19 Dec 2019 10:52:54 +0000 Subject: [PATCH 267/686] Mute SnapshotLifecycleRestIT testFullPolicySnapshot Relates to #50358 --- .../org/elasticsearch/xpack/slm/SnapshotLifecycleRestIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleRestIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleRestIT.java index cb37d3ad051ad..07628b54c960e 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleRestIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleRestIT.java @@ -82,6 +82,7 @@ public void testMissingRepo() throws Exception { } @SuppressWarnings("unchecked") + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50358") public void testFullPolicySnapshot() throws Exception { final String indexName = "test"; final String policyName = "test-policy"; From 727f641583b788d604b12bdddf74a67dd6236a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 19 Dec 2019 12:19:44 +0100 Subject: [PATCH 268/686] [DOCS] Adds inference processor documentation (#50204) Co-Authored-By: Lisa Cawley --- docs/reference/ingest/ingest-node.asciidoc | 1 + .../ingest/processors/inference.asciidoc | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 docs/reference/ingest/processors/inference.asciidoc diff --git a/docs/reference/ingest/ingest-node.asciidoc b/docs/reference/ingest/ingest-node.asciidoc index 596bda67d3ed8..d32384e613bb7 100644 --- a/docs/reference/ingest/ingest-node.asciidoc +++ b/docs/reference/ingest/ingest-node.asciidoc @@ -838,6 +838,7 @@ include::processors/geoip.asciidoc[] include::processors/grok.asciidoc[] include::processors/gsub.asciidoc[] include::processors/html_strip.asciidoc[] +include::processors/inference.asciidoc[] include::processors/join.asciidoc[] include::processors/json.asciidoc[] include::processors/kv.asciidoc[] diff --git a/docs/reference/ingest/processors/inference.asciidoc b/docs/reference/ingest/processors/inference.asciidoc new file mode 100644 index 0000000000000..799e465f87a48 --- /dev/null +++ b/docs/reference/ingest/processors/inference.asciidoc @@ -0,0 +1,107 @@ +[role="xpack"] +[testenv="basic"] +[[inference-processor]] +=== {infer-cap} Processor + +Uses a pre-trained {dfanalytics} model to infer against the data that is being +ingested in the pipeline. + + +[[inference-options]] +.{infer-cap} Options +[options="header"] +|====== +| Name | Required | Default | Description +| `model_id` | yes | - | (String) The ID of the model to load and infer against. +| `target_field` | no | `ml.inference.` | (String) Field added to incoming documents to contain results objects. +| `field_mappings` | yes | - | (Object) Maps the document field names to the known field names of the model. +| `inference_config` | yes | - | (Object) Contains the inference type and its options. There are two types: <> and <>. +include::common-options.asciidoc[] +|====== + + +[source,js] +-------------------------------------------------- +{ + "inference": { + "model_id": "flight_delay_regression-1571767128603", + "target_field": "FlightDelayMin_prediction_infer", + "field_mappings": {}, + "inference_config": {"regression": {}}, + "model_info_field": "ml" + } +} +-------------------------------------------------- +// NOTCONSOLE + + + +[discrete] +[[inference-processor-regression-opt]] +==== {regression-cap} configuration options + +`results_field`:: +(Optional, string) +Specifies the field to which the inference prediction is written. Defaults to +`predicted_value`. + + +[discrete] +[[inference-processor-classification-opt]] +==== {classification-cap} configuration options + +`results_field`:: +(Optional, string) +The field that is added to incoming documents to contain the inference prediction. Defaults to +`predicted_value`. + +`num_top_classes`:: +(Optional, integer) +Specifies the number of top class predictions to return. Defaults to 0. + +`top_classes_results_field`:: +(Optional, string) +Specifies the field to which the top classes are written. Defaults to +`top_classes`. + + +[discrete] +[[inference-processor-config-example]] +==== `inference_config` examples + +[source,js] +-------------------------------------------------- +{ + "inference_config": { + “regressionâ€: { + “results_fieldâ€: “my_regression†+ } + }, +} +-------------------------------------------------- +// NOTCONSOLE + +This configuration specifies a `regression` inference and the results are +written to the `my_regression` field contained in the `target_field` results +object. + + +[source,js] +-------------------------------------------------- +{ + "inference_config": { + “classificationâ€: { + “num_top_classesâ€: 2, + “results_fieldâ€: “predictionâ€, + “top_classes_results_fieldâ€: “probabilities†+ } + } +} +-------------------------------------------------- +// NOTCONSOLE + +This configuration specifies a `classification` inference. The number of +categories for which the predicted probabilities are reported is 2 +(`num_top_classes`). The result is written to the `prediction` field and the top +classes to the `probabilities` field. Both fields are contained in the +`target_field` results object. From 97a23a9edbffa5a1bb8af9bc78fbb86317b1ca19 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Thu, 19 Dec 2019 11:31:43 +0000 Subject: [PATCH 269/686] Fixes to task result index mapping (#50359) The built-in mapping for the tasks result index still has a mapping type defined; while this does not matter for index creation, as we still have a create method that takes a top-level type, it does matter for updates. In combination with a separate bug, that the built-in mapping has not incremented its meta version, this meant that tasks submitted to a cluster with an already existing task index would attempt to update the mappings on that index, and fail due to the top-level type. This commit fixes the mapping to have a top-level mapping of _doc, and also updates the meta version so that we do not update mappings on every new task. It also adds a test that explicitly runs two asynchronous tasks to ensure that the mappings do not cause a failure. Fixes #50248 --- .../java/org/elasticsearch/tasks/TaskResultsService.java | 4 +--- .../org/elasticsearch/tasks/task-index-mapping.json | 4 ++-- .../action/admin/cluster/node/tasks/TasksIT.java | 7 +++++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java b/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java index bcc7d62fd4c22..6e1b5ec2d9e00 100644 --- a/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java +++ b/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java @@ -67,8 +67,6 @@ public class TaskResultsService { public static final String TASK_INDEX = ".tasks"; - public static final String TASK_TYPE = "task"; - public static final String TASK_RESULT_INDEX_MAPPING_FILE = "task-index-mapping.json"; public static final String TASK_RESULT_MAPPING_VERSION_META_FIELD = "version"; @@ -103,7 +101,7 @@ public void storeResult(TaskResult taskResult, ActionListener listener) { CreateIndexRequest createIndexRequest = new CreateIndexRequest(); createIndexRequest.settings(taskResultIndexSettings()); createIndexRequest.index(TASK_INDEX); - createIndexRequest.mapping(TASK_TYPE, taskResultIndexMapping(), XContentType.JSON); + createIndexRequest.mapping(taskResultIndexMapping()); createIndexRequest.cause("auto(task api)"); client.admin().indices().create(createIndexRequest, new ActionListener() { diff --git a/server/src/main/resources/org/elasticsearch/tasks/task-index-mapping.json b/server/src/main/resources/org/elasticsearch/tasks/task-index-mapping.json index b8ef2dcd56216..ef5873ae53c58 100644 --- a/server/src/main/resources/org/elasticsearch/tasks/task-index-mapping.json +++ b/server/src/main/resources/org/elasticsearch/tasks/task-index-mapping.json @@ -1,7 +1,7 @@ { - "task" : { + "_doc" : { "_meta": { - "version": 2 + "version": 3 }, "dynamic" : "strict", "properties" : { diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TasksIT.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TasksIT.java index 21c2c5da15137..a102cb4bc2082 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TasksIT.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TasksIT.java @@ -768,6 +768,13 @@ public void testTaskStoringSuccessfulResult() throws Exception { GetTaskResponse getResponse = expectFinishedTask(taskId); assertEquals(result, getResponse.getTask().getResponseAsMap()); assertNull(getResponse.getTask().getError()); + + // run it again to check that the tasks index has been successfully created and can be re-used + client().execute(TestTaskPlugin.TestTaskAction.INSTANCE, request).get(); + + events = findEvents(TestTaskPlugin.TestTaskAction.NAME, Tuple::v1); + + assertEquals(2, events.size()); } public void testTaskStoringFailureResult() throws Exception { From 5268edd0f44e8f0dd8420c63340aa3595eae629a Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Thu, 19 Dec 2019 07:12:04 -0500 Subject: [PATCH 270/686] [ML][Inference] points HLRC javadocs to reference docs (#50321) --- .../elasticsearch/client/MachineLearningClient.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java index 0589f5c28d865..0a71b8ddb0172 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java @@ -2301,7 +2301,7 @@ public Cancellable explainDataFrameAnalyticsAsync(ExplainDataFrameAnalyticsReque * Gets trained model configs *

    * For additional info - * see + * see * GET Trained Model Configs documentation * * @param request The {@link GetTrainedModelsRequest} @@ -2321,7 +2321,7 @@ public GetTrainedModelsResponse getTrainedModels(GetTrainedModelsRequest request * Gets trained model configs asynchronously and notifies listener upon completion *

    * For additional info - * see + * see * GET Trained Model Configs documentation * * @param request The {@link GetTrainedModelsRequest} @@ -2344,7 +2344,7 @@ public Cancellable getTrainedModelsAsync(GetTrainedModelsRequest request, * Gets trained model stats *

    * For additional info - * see + * see * GET Trained Model Stats documentation * * @param request The {@link GetTrainedModelsStatsRequest} @@ -2364,7 +2364,7 @@ public GetTrainedModelsStatsResponse getTrainedModelsStats(GetTrainedModelsStats * Gets trained model stats asynchronously and notifies listener upon completion *

    * For additional info - * see + * see * GET Trained Model Stats documentation * * @param request The {@link GetTrainedModelsStatsRequest} @@ -2387,7 +2387,7 @@ public Cancellable getTrainedModelsStatsAsync(GetTrainedModelsStatsRequest reque * Deletes the given Trained Model *

    * For additional info - * see + * see * DELETE Trained Model documentation * * @param request The {@link DeleteTrainedModelRequest} @@ -2407,7 +2407,7 @@ public AcknowledgedResponse deleteTrainedModel(DeleteTrainedModelRequest request * Deletes the given Trained Model asynchronously and notifies listener upon completion *

    * For additional info - * see + * see * DELETE Trained Model documentation * * @param request The {@link DeleteTrainedModelRequest} From d1b40000f9f8f608d039f0c6bc32721b6b875091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Thu, 19 Dec 2019 16:07:09 +0100 Subject: [PATCH 271/686] Implement `precision` and `recall` metrics for classification evaluation (#49671) --- .../client/ml/EvaluateDataFrameResponse.java | 7 +- .../MlEvaluationNamedXContentProvider.java | 106 ++++-- .../classification/Classification.java | 12 +- .../classification/PrecisionMetric.java | 201 ++++++++++ .../classification/RecallMetric.java | 201 ++++++++++ .../evaluation/regression/Regression.java | 12 +- .../BinarySoftClassification.java | 4 +- .../client/MachineLearningIT.java | 64 ++++ .../client/RestHighLevelClientTests.java | 49 ++- .../MlClientDocumentationIT.java | 28 +- .../ml/EvaluateDataFrameResponseTests.java | 2 + .../classification/ClassificationTests.java | 2 + .../PrecisionMetricResultTests.java | 67 ++++ .../classification/PrecisionMetricTests.java | 53 +++ .../RecallMetricResultTests.java | 67 ++++ .../classification/RecallMetricTests.java | 53 +++ .../ml/evaluate-data-frame.asciidoc | 16 +- .../xpack/core/XPackClientPlugin.java | 34 +- .../ml/dataframe/evaluation/Evaluation.java | 7 +- .../evaluation/EvaluationMetric.java | 4 +- .../MlEvaluationNamedXContentProvider.java | 222 +++++++---- .../evaluation/classification/Accuracy.java | 26 +- .../classification/Classification.java | 21 +- .../classification/ClassificationMetric.java | 11 - .../MulticlassConfusionMatrix.java | 48 ++- .../evaluation/classification/Precision.java | 345 ++++++++++++++++++ .../evaluation/classification/Recall.java | 319 ++++++++++++++++ .../regression/MeanSquaredError.java | 19 +- .../evaluation/regression/RSquared.java | 23 +- .../evaluation/regression/Regression.java | 21 +- .../regression/RegressionMetric.java | 11 - .../AbstractConfusionMatrixMetric.java | 13 +- .../evaluation/softclassification/AucRoc.java | 20 +- .../BinarySoftClassification.java | 25 +- .../softclassification/ConfusionMatrix.java | 6 +- .../softclassification/Precision.java | 4 +- .../evaluation/softclassification/Recall.java | 4 +- .../ScoreByThresholdResult.java | 7 +- .../SoftClassificationMetric.java | 17 - .../EvaluateDataFrameActionRequestTests.java | 11 +- .../EvaluateDataFrameActionResponseTests.java | 11 +- .../classification/AccuracyResultTests.java | 24 +- .../classification/ClassificationTests.java | 23 +- .../MulticlassConfusionMatrixTests.java | 12 +- .../classification/PrecisionResultTests.java | 48 +++ .../classification/PrecisionTests.java | 119 ++++++ .../classification/RecallResultTests.java | 48 +++ .../classification/RecallTests.java | 118 ++++++ .../classification/TupleMatchers.java | 49 +++ .../regression/RegressionTests.java | 5 +- .../BinarySoftClassificationTests.java | 5 +- .../ClassificationEvaluationIT.java | 149 ++++++-- .../ml/integration/ClassificationIT.java | 26 +- .../test/ml/evaluate_data_frame.yml | 52 +++ 54 files changed, 2483 insertions(+), 368 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetric.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetric.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetricResultTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetricTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetricResultTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetricTests.java delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationMetric.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RegressionMetric.java delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/SoftClassificationMetric.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionResultTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallResultTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/TupleMatchers.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/EvaluateDataFrameResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/EvaluateDataFrameResponse.java index 0709021ed4bd5..b53e692315507 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/EvaluateDataFrameResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/EvaluateDataFrameResponse.java @@ -34,6 +34,7 @@ import java.util.Objects; import java.util.stream.Collectors; +import static org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; public class EvaluateDataFrameResponse implements ToXContentObject { @@ -46,7 +47,7 @@ public static EvaluateDataFrameResponse fromXContent(XContentParser parser) thro ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation); String evaluationName = parser.currentName(); parser.nextToken(); - Map metrics = parser.map(LinkedHashMap::new, EvaluateDataFrameResponse::parseMetric); + Map metrics = parser.map(LinkedHashMap::new, p -> parseMetric(evaluationName, p)); List knownMetrics = metrics.values().stream() .filter(Objects::nonNull) // Filter out null values returned by {@link EvaluateDataFrameResponse::parseMetric}. @@ -55,10 +56,10 @@ public static EvaluateDataFrameResponse fromXContent(XContentParser parser) thro return new EvaluateDataFrameResponse(evaluationName, knownMetrics); } - private static EvaluationMetric.Result parseMetric(XContentParser parser) throws IOException { + private static EvaluationMetric.Result parseMetric(String evaluationName, XContentParser parser) throws IOException { String metricName = parser.currentName(); try { - return parser.namedObject(EvaluationMetric.Result.class, metricName, null); + return parser.namedObject(EvaluationMetric.Result.class, registeredMetricName(evaluationName, metricName), null); } catch (NamedObjectNotFoundException e) { parser.skipChildren(); // Metric name not recognized. Return {@code null} value here and filter it out later. diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/MlEvaluationNamedXContentProvider.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/MlEvaluationNamedXContentProvider.java index efe58b9739eda..cd5c2abdf5627 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/MlEvaluationNamedXContentProvider.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/MlEvaluationNamedXContentProvider.java @@ -20,24 +20,36 @@ import org.elasticsearch.client.ml.dataframe.evaluation.classification.AccuracyMetric; import org.elasticsearch.client.ml.dataframe.evaluation.classification.Classification; -import org.elasticsearch.client.ml.dataframe.evaluation.regression.MeanSquaredErrorMetric; import org.elasticsearch.client.ml.dataframe.evaluation.classification.MulticlassConfusionMatrixMetric; +import org.elasticsearch.client.ml.dataframe.evaluation.regression.MeanSquaredErrorMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.RSquaredMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.Regression; -import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.BinarySoftClassification; -import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; -import org.elasticsearch.plugins.spi.NamedXContentProvider; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.AucRocMetric; +import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.BinarySoftClassification; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.ConfusionMatrixMetric; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.PrecisionMetric; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.RecallMetric; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.plugins.spi.NamedXContentProvider; import java.util.Arrays; import java.util.List; public class MlEvaluationNamedXContentProvider implements NamedXContentProvider { + /** + * Constructs the name under which a metric (or metric result) is registered. + * The name is prefixed with evaluation name so that registered names are unique. + * + * @param evaluationName name of the evaluation + * @param metricName name of the metric + * @return name appropriate for registering a metric (or metric result) in {@link NamedXContentRegistry} + */ + public static String registeredMetricName(String evaluationName, String metricName) { + return evaluationName + "." + metricName; + } + @Override public List getNamedXContentParsers() { return Arrays.asList( @@ -47,39 +59,91 @@ Evaluation.class, new ParseField(BinarySoftClassification.NAME), BinarySoftClass new NamedXContentRegistry.Entry(Evaluation.class, new ParseField(Classification.NAME), Classification::fromXContent), new NamedXContentRegistry.Entry(Evaluation.class, new ParseField(Regression.NAME), Regression::fromXContent), // Evaluation metrics - new NamedXContentRegistry.Entry(EvaluationMetric.class, new ParseField(AucRocMetric.NAME), AucRocMetric::fromXContent), - new NamedXContentRegistry.Entry(EvaluationMetric.class, new ParseField(PrecisionMetric.NAME), PrecisionMetric::fromXContent), - new NamedXContentRegistry.Entry(EvaluationMetric.class, new ParseField(RecallMetric.NAME), RecallMetric::fromXContent), new NamedXContentRegistry.Entry( - EvaluationMetric.class, new ParseField(ConfusionMatrixMetric.NAME), ConfusionMatrixMetric::fromXContent), + EvaluationMetric.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, AucRocMetric.NAME)), + AucRocMetric::fromXContent), new NamedXContentRegistry.Entry( - EvaluationMetric.class, new ParseField(AccuracyMetric.NAME), AccuracyMetric::fromXContent), + EvaluationMetric.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, PrecisionMetric.NAME)), + PrecisionMetric::fromXContent), new NamedXContentRegistry.Entry( EvaluationMetric.class, - new ParseField(MulticlassConfusionMatrixMetric.NAME), + new ParseField(registeredMetricName(BinarySoftClassification.NAME, RecallMetric.NAME)), + RecallMetric::fromXContent), + new NamedXContentRegistry.Entry( + EvaluationMetric.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, ConfusionMatrixMetric.NAME)), + ConfusionMatrixMetric::fromXContent), + new NamedXContentRegistry.Entry( + EvaluationMetric.class, + new ParseField(registeredMetricName(Classification.NAME, AccuracyMetric.NAME)), + AccuracyMetric::fromXContent), + new NamedXContentRegistry.Entry( + EvaluationMetric.class, + new ParseField(registeredMetricName( + Classification.NAME, org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.NAME)), + org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric::fromXContent), + new NamedXContentRegistry.Entry( + EvaluationMetric.class, + new ParseField(registeredMetricName( + Classification.NAME, org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.NAME)), + org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric::fromXContent), + new NamedXContentRegistry.Entry( + EvaluationMetric.class, + new ParseField(registeredMetricName(Classification.NAME, MulticlassConfusionMatrixMetric.NAME)), MulticlassConfusionMatrixMetric::fromXContent), new NamedXContentRegistry.Entry( - EvaluationMetric.class, new ParseField(MeanSquaredErrorMetric.NAME), MeanSquaredErrorMetric::fromXContent), + EvaluationMetric.class, + new ParseField(registeredMetricName(Regression.NAME, MeanSquaredErrorMetric.NAME)), + MeanSquaredErrorMetric::fromXContent), new NamedXContentRegistry.Entry( - EvaluationMetric.class, new ParseField(RSquaredMetric.NAME), RSquaredMetric::fromXContent), + EvaluationMetric.class, + new ParseField(registeredMetricName(Regression.NAME, RSquaredMetric.NAME)), + RSquaredMetric::fromXContent), // Evaluation metrics results new NamedXContentRegistry.Entry( - EvaluationMetric.Result.class, new ParseField(AucRocMetric.NAME), AucRocMetric.Result::fromXContent), + EvaluationMetric.Result.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, AucRocMetric.NAME)), + AucRocMetric.Result::fromXContent), + new NamedXContentRegistry.Entry( + EvaluationMetric.Result.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, PrecisionMetric.NAME)), + PrecisionMetric.Result::fromXContent), new NamedXContentRegistry.Entry( - EvaluationMetric.Result.class, new ParseField(PrecisionMetric.NAME), PrecisionMetric.Result::fromXContent), + EvaluationMetric.Result.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, RecallMetric.NAME)), + RecallMetric.Result::fromXContent), new NamedXContentRegistry.Entry( - EvaluationMetric.Result.class, new ParseField(RecallMetric.NAME), RecallMetric.Result::fromXContent), + EvaluationMetric.Result.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, ConfusionMatrixMetric.NAME)), + ConfusionMatrixMetric.Result::fromXContent), new NamedXContentRegistry.Entry( - EvaluationMetric.Result.class, new ParseField(ConfusionMatrixMetric.NAME), ConfusionMatrixMetric.Result::fromXContent), + EvaluationMetric.Result.class, + new ParseField(registeredMetricName(Classification.NAME, AccuracyMetric.NAME)), + AccuracyMetric.Result::fromXContent), new NamedXContentRegistry.Entry( - EvaluationMetric.Result.class, new ParseField(AccuracyMetric.NAME), AccuracyMetric.Result::fromXContent), + EvaluationMetric.Result.class, + new ParseField(registeredMetricName( + Classification.NAME, org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.NAME)), + org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.Result::fromXContent), new NamedXContentRegistry.Entry( EvaluationMetric.Result.class, - new ParseField(MulticlassConfusionMatrixMetric.NAME), + new ParseField(registeredMetricName( + Classification.NAME, org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.NAME)), + org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.Result::fromXContent), + new NamedXContentRegistry.Entry( + EvaluationMetric.Result.class, + new ParseField(registeredMetricName(Classification.NAME, MulticlassConfusionMatrixMetric.NAME)), MulticlassConfusionMatrixMetric.Result::fromXContent), new NamedXContentRegistry.Entry( - EvaluationMetric.Result.class, new ParseField(MeanSquaredErrorMetric.NAME), MeanSquaredErrorMetric.Result::fromXContent), + EvaluationMetric.Result.class, + new ParseField(registeredMetricName(Regression.NAME, MeanSquaredErrorMetric.NAME)), + MeanSquaredErrorMetric.Result::fromXContent), new NamedXContentRegistry.Entry( - EvaluationMetric.Result.class, new ParseField(RSquaredMetric.NAME), RSquaredMetric.Result::fromXContent)); + EvaluationMetric.Result.class, + new ParseField(registeredMetricName(Regression.NAME, RSquaredMetric.NAME)), + RSquaredMetric.Result::fromXContent) + ); } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/Classification.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/Classification.java index d7466fcc023b5..f64078228986b 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/Classification.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/Classification.java @@ -32,6 +32,10 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + /** * Evaluation of classification results. */ @@ -48,10 +52,10 @@ public class Classification implements Evaluation { NAME, true, a -> new Classification((String) a[0], (String) a[1], (List) a[2])); static { - PARSER.declareString(ConstructingObjectParser.constructorArg(), ACTUAL_FIELD); - PARSER.declareString(ConstructingObjectParser.constructorArg(), PREDICTED_FIELD); - PARSER.declareNamedObjects(ConstructingObjectParser.optionalConstructorArg(), - (p, c, n) -> p.namedObject(EvaluationMetric.class, n, c), METRICS); + PARSER.declareString(constructorArg(), ACTUAL_FIELD); + PARSER.declareString(constructorArg(), PREDICTED_FIELD); + PARSER.declareNamedObjects( + optionalConstructorArg(), (p, c, n) -> p.namedObject(EvaluationMetric.class, registeredMetricName(NAME, n), c), METRICS); } public static Classification fromXContent(XContentParser parser) { diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetric.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetric.java new file mode 100644 index 0000000000000..8eff7986dcc36 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetric.java @@ -0,0 +1,201 @@ +/* + * 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. + */ +package org.elasticsearch.client.ml.dataframe.evaluation.classification; + +import org.elasticsearch.client.ml.dataframe.evaluation.EvaluationMetric; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +/** + * {@link PrecisionMetric} is a metric that answers the question: + * "What fraction of documents classified as X actually belongs to X?" + * for any given class X + * + * equation: precision(X) = TP(X) / (TP(X) + FP(X)) + * where: TP(X) - number of true positives wrt X + * FP(X) - number of false positives wrt X + */ +public class PrecisionMetric implements EvaluationMetric { + + public static final String NAME = "precision"; + + private static final ObjectParser PARSER = new ObjectParser<>(NAME, true, PrecisionMetric::new); + + public static PrecisionMetric fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + public PrecisionMetric() {} + + @Override + public String getName() { + return NAME; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return true; + } + + @Override + public int hashCode() { + return Objects.hashCode(NAME); + } + + public static class Result implements EvaluationMetric.Result { + + private static final ParseField CLASSES = new ParseField("classes"); + private static final ParseField AVG_PRECISION = new ParseField("avg_precision"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("precision_result", true, a -> new Result((List) a[0], (double) a[1])); + + static { + PARSER.declareObjectArray(constructorArg(), PerClassResult.PARSER, CLASSES); + PARSER.declareDouble(constructorArg(), AVG_PRECISION); + } + + public static Result fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + /** List of per-class results. */ + private final List classes; + /** Average of per-class precisions. */ + private final double avgPrecision; + + public Result(List classes, double avgPrecision) { + this.classes = Collections.unmodifiableList(Objects.requireNonNull(classes)); + this.avgPrecision = avgPrecision; + } + + @Override + public String getMetricName() { + return NAME; + } + + public List getClasses() { + return classes; + } + + public double getAvgPrecision() { + return avgPrecision; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(CLASSES.getPreferredName(), classes); + builder.field(AVG_PRECISION.getPreferredName(), avgPrecision); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Result that = (Result) o; + return Objects.equals(this.classes, that.classes) + && this.avgPrecision == that.avgPrecision; + } + + @Override + public int hashCode() { + return Objects.hash(classes, avgPrecision); + } + } + + public static class PerClassResult implements ToXContentObject { + + private static final ParseField CLASS_NAME = new ParseField("class_name"); + private static final ParseField PRECISION = new ParseField("precision"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("precision_per_class_result", true, a -> new PerClassResult((String) a[0], (double) a[1])); + + static { + PARSER.declareString(constructorArg(), CLASS_NAME); + PARSER.declareDouble(constructorArg(), PRECISION); + } + + /** Name of the class. */ + private final String className; + /** Fraction of documents predicted as belonging to the {@code predictedClass} class predicted correctly. */ + private final double precision; + + public PerClassResult(String className, double precision) { + this.className = Objects.requireNonNull(className); + this.precision = precision; + } + + public String getClassName() { + return className; + } + + public double getPrecision() { + return precision; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(CLASS_NAME.getPreferredName(), className); + builder.field(PRECISION.getPreferredName(), precision); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PerClassResult that = (PerClassResult) o; + return Objects.equals(this.className, that.className) + && this.precision == that.precision; + } + + @Override + public int hashCode() { + return Objects.hash(className, precision); + } + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetric.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetric.java new file mode 100644 index 0000000000000..d46a70da8c3f6 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetric.java @@ -0,0 +1,201 @@ +/* + * 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. + */ +package org.elasticsearch.client.ml.dataframe.evaluation.classification; + +import org.elasticsearch.client.ml.dataframe.evaluation.EvaluationMetric; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +/** + * {@link RecallMetric} is a metric that answers the question: + * "What fraction of documents belonging to X have been predicted as X by the classifier?" + * for any given class X + * + * equation: recall(X) = TP(X) / (TP(X) + FN(X)) + * where: TP(X) - number of true positives wrt X + * FN(X) - number of false negatives wrt X + */ +public class RecallMetric implements EvaluationMetric { + + public static final String NAME = "recall"; + + private static final ObjectParser PARSER = new ObjectParser<>(NAME, true, RecallMetric::new); + + public static RecallMetric fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + public RecallMetric() {} + + @Override + public String getName() { + return NAME; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return true; + } + + @Override + public int hashCode() { + return Objects.hashCode(NAME); + } + + public static class Result implements EvaluationMetric.Result { + + private static final ParseField CLASSES = new ParseField("classes"); + private static final ParseField AVG_RECALL = new ParseField("avg_recall"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("recall_result", true, a -> new Result((List) a[0], (double) a[1])); + + static { + PARSER.declareObjectArray(constructorArg(), PerClassResult.PARSER, CLASSES); + PARSER.declareDouble(constructorArg(), AVG_RECALL); + } + + public static Result fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + /** List of per-class results. */ + private final List classes; + /** Average of per-class recalls. */ + private final double avgRecall; + + public Result(List classes, double avgRecall) { + this.classes = Collections.unmodifiableList(Objects.requireNonNull(classes)); + this.avgRecall = avgRecall; + } + + @Override + public String getMetricName() { + return NAME; + } + + public List getClasses() { + return classes; + } + + public double getAvgRecall() { + return avgRecall; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(CLASSES.getPreferredName(), classes); + builder.field(AVG_RECALL.getPreferredName(), avgRecall); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Result that = (Result) o; + return Objects.equals(this.classes, that.classes) + && this.avgRecall == that.avgRecall; + } + + @Override + public int hashCode() { + return Objects.hash(classes, avgRecall); + } + } + + public static class PerClassResult implements ToXContentObject { + + private static final ParseField CLASS_NAME = new ParseField("class_name"); + private static final ParseField RECALL = new ParseField("recall"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("recall_per_class_result", true, a -> new PerClassResult((String) a[0], (double) a[1])); + + static { + PARSER.declareString(constructorArg(), CLASS_NAME); + PARSER.declareDouble(constructorArg(), RECALL); + } + + /** Name of the class. */ + private final String className; + /** Fraction of documents actually belonging to the {@code actualClass} class predicted correctly. */ + private final double recall; + + public PerClassResult(String className, double recall) { + this.className = Objects.requireNonNull(className); + this.recall = recall; + } + + public String getClassName() { + return className; + } + + public double getRecall() { + return recall; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(CLASS_NAME.getPreferredName(), className); + builder.field(RECALL.getPreferredName(), recall); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PerClassResult that = (PerClassResult) o; + return Objects.equals(this.className, that.className) + && this.recall == that.recall; + } + + @Override + public int hashCode() { + return Objects.hash(className, recall); + } + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/regression/Regression.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/regression/Regression.java index 79b9ab6eb1dd5..1d8b5bcdb0902 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/regression/Regression.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/regression/Regression.java @@ -33,6 +33,10 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + /** * Evaluation of regression results. */ @@ -49,10 +53,10 @@ public class Regression implements Evaluation { NAME, true, a -> new Regression((String) a[0], (String) a[1], (List) a[2])); static { - PARSER.declareString(ConstructingObjectParser.constructorArg(), ACTUAL_FIELD); - PARSER.declareString(ConstructingObjectParser.constructorArg(), PREDICTED_FIELD); - PARSER.declareNamedObjects(ConstructingObjectParser.optionalConstructorArg(), - (p, c, n) -> p.namedObject(EvaluationMetric.class, n, c), METRICS); + PARSER.declareString(constructorArg(), ACTUAL_FIELD); + PARSER.declareString(constructorArg(), PREDICTED_FIELD); + PARSER.declareNamedObjects( + optionalConstructorArg(), (p, c, n) -> p.namedObject(EvaluationMetric.class, registeredMetricName(NAME, n), c), METRICS); } public static Regression fromXContent(XContentParser parser) { diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/BinarySoftClassification.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/BinarySoftClassification.java index cb531c6ab044a..b75af7cec11f6 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/BinarySoftClassification.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/softclassification/BinarySoftClassification.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; @@ -59,7 +60,8 @@ public class BinarySoftClassification implements Evaluation { static { PARSER.declareString(constructorArg(), ACTUAL_FIELD); PARSER.declareString(constructorArg(), PREDICTED_PROBABILITY_FIELD); - PARSER.declareNamedObjects(optionalConstructorArg(), (p, c, n) -> p.namedObject(EvaluationMetric.class, n, null), METRICS); + PARSER.declareNamedObjects( + optionalConstructorArg(), (p, c, n) -> p.namedObject(EvaluationMetric.class, registeredMetricName(NAME, n), null), METRICS); } public static BinarySoftClassification fromXContent(XContentParser parser) { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index 29e69c5095cbd..4aee48dff13a5 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -1830,6 +1830,70 @@ public void testEvaluateDataFrame_Classification() throws IOException { new AccuracyMetric.ActualClass("ant", 1, 0.0)))); assertThat(accuracyResult.getOverallAccuracy(), equalTo(0.6)); // 6 out of 10 examples were classified correctly } + { // Precision + EvaluateDataFrameRequest evaluateDataFrameRequest = + new EvaluateDataFrameRequest( + indexName, + null, + new Classification( + actualClassField, + predictedClassField, + new org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric())); + + EvaluateDataFrameResponse evaluateDataFrameResponse = + execute(evaluateDataFrameRequest, machineLearningClient::evaluateDataFrame, machineLearningClient::evaluateDataFrameAsync); + assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME)); + assertThat(evaluateDataFrameResponse.getMetrics().size(), equalTo(1)); + + org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.Result precisionResult = + evaluateDataFrameResponse.getMetricByName( + org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.NAME); + assertThat( + precisionResult.getMetricName(), + equalTo(org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.NAME)); + assertThat( + precisionResult.getClasses(), + equalTo( + List.of( + // 3 out of 5 examples labeled as "cat" were classified correctly + new org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.PerClassResult("cat", 0.6), + // 3 out of 4 examples labeled as "dog" were classified correctly + new org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.PerClassResult("dog", 0.75)))); + assertThat(precisionResult.getAvgPrecision(), equalTo(0.675)); + } + { // Recall + EvaluateDataFrameRequest evaluateDataFrameRequest = + new EvaluateDataFrameRequest( + indexName, + null, + new Classification( + actualClassField, + predictedClassField, + new org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric())); + + EvaluateDataFrameResponse evaluateDataFrameResponse = + execute(evaluateDataFrameRequest, machineLearningClient::evaluateDataFrame, machineLearningClient::evaluateDataFrameAsync); + assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME)); + assertThat(evaluateDataFrameResponse.getMetrics().size(), equalTo(1)); + + org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.Result recallResult = + evaluateDataFrameResponse.getMetricByName( + org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.NAME); + assertThat( + recallResult.getMetricName(), + equalTo(org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.NAME)); + assertThat( + recallResult.getClasses(), + equalTo( + List.of( + // 3 out of 5 examples labeled as "cat" were classified correctly + new org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.PerClassResult("cat", 0.6), + // 3 out of 4 examples labeled as "dog" were classified correctly + new org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.PerClassResult("dog", 0.75), + // no examples labeled as "ant" were classified correctly + new org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.PerClassResult("ant", 0.0)))); + assertThat(recallResult.getAvgRecall(), equalTo(0.45)); + } { // No size provided for MulticlassConfusionMatrixMetric, default used instead EvaluateDataFrameRequest evaluateDataFrameRequest = new EvaluateDataFrameRequest( diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index 3135239530199..4039ec251d335 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -128,6 +128,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.equalTo; @@ -688,7 +689,7 @@ public void testDefaultNamedXContents() { public void testProvidedNamedXContents() { List namedXContents = RestHighLevelClient.getProvidedNamedXContents(); - assertEquals(51, namedXContents.size()); + assertEquals(55, namedXContents.size()); Map, Integer> categories = new HashMap<>(); List names = new ArrayList<>(); for (NamedXContentRegistry.Entry namedXContent : namedXContents) { @@ -730,26 +731,36 @@ public void testProvidedNamedXContents() { assertTrue(names.contains(TimeSyncConfig.NAME)); assertEquals(Integer.valueOf(3), categories.get(org.elasticsearch.client.ml.dataframe.evaluation.Evaluation.class)); assertThat(names, hasItems(BinarySoftClassification.NAME, Classification.NAME, Regression.NAME)); - assertEquals(Integer.valueOf(8), categories.get(org.elasticsearch.client.ml.dataframe.evaluation.EvaluationMetric.class)); + assertEquals(Integer.valueOf(10), categories.get(org.elasticsearch.client.ml.dataframe.evaluation.EvaluationMetric.class)); assertThat(names, - hasItems(AucRocMetric.NAME, - PrecisionMetric.NAME, - RecallMetric.NAME, - ConfusionMatrixMetric.NAME, - AccuracyMetric.NAME, - MulticlassConfusionMatrixMetric.NAME, - MeanSquaredErrorMetric.NAME, - RSquaredMetric.NAME)); - assertEquals(Integer.valueOf(8), categories.get(org.elasticsearch.client.ml.dataframe.evaluation.EvaluationMetric.Result.class)); + hasItems( + registeredMetricName(BinarySoftClassification.NAME, AucRocMetric.NAME), + registeredMetricName(BinarySoftClassification.NAME, PrecisionMetric.NAME), + registeredMetricName(BinarySoftClassification.NAME, RecallMetric.NAME), + registeredMetricName(BinarySoftClassification.NAME, ConfusionMatrixMetric.NAME), + registeredMetricName(Classification.NAME, AccuracyMetric.NAME), + registeredMetricName( + Classification.NAME, org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.NAME), + registeredMetricName( + Classification.NAME, org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.NAME), + registeredMetricName(Classification.NAME, MulticlassConfusionMatrixMetric.NAME), + registeredMetricName(Regression.NAME, MeanSquaredErrorMetric.NAME), + registeredMetricName(Regression.NAME, RSquaredMetric.NAME))); + assertEquals(Integer.valueOf(10), categories.get(org.elasticsearch.client.ml.dataframe.evaluation.EvaluationMetric.Result.class)); assertThat(names, - hasItems(AucRocMetric.NAME, - PrecisionMetric.NAME, - RecallMetric.NAME, - ConfusionMatrixMetric.NAME, - AccuracyMetric.NAME, - MulticlassConfusionMatrixMetric.NAME, - MeanSquaredErrorMetric.NAME, - RSquaredMetric.NAME)); + hasItems( + registeredMetricName(BinarySoftClassification.NAME, AucRocMetric.NAME), + registeredMetricName(BinarySoftClassification.NAME, PrecisionMetric.NAME), + registeredMetricName(BinarySoftClassification.NAME, RecallMetric.NAME), + registeredMetricName(BinarySoftClassification.NAME, ConfusionMatrixMetric.NAME), + registeredMetricName(Classification.NAME, AccuracyMetric.NAME), + registeredMetricName( + Classification.NAME, org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.NAME), + registeredMetricName( + Classification.NAME, org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.NAME), + registeredMetricName(Classification.NAME, MulticlassConfusionMatrixMetric.NAME), + registeredMetricName(Regression.NAME, MeanSquaredErrorMetric.NAME), + registeredMetricName(Regression.NAME, RSquaredMetric.NAME))); assertEquals(Integer.valueOf(3), categories.get(org.elasticsearch.client.ml.inference.preprocessing.PreProcessor.class)); assertThat(names, hasItems(FrequencyEncoding.NAME, OneHotEncoding.NAME, TargetMeanEncoding.NAME)); assertEquals(Integer.valueOf(2), categories.get(org.elasticsearch.client.ml.inference.trainedmodel.TrainedModel.class)); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 13185e221633b..12fea5f5c5658 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -3372,7 +3372,9 @@ public void testEvaluateDataFrame_Classification() throws Exception { "predicted_class", // <3> // Evaluation metrics // <4> new AccuracyMetric(), // <5> - new MulticlassConfusionMatrixMetric(3)); // <6> + new org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric(), // <6> + new org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric(), // <7> + new MulticlassConfusionMatrixMetric(3)); // <8> // end::evaluate-data-frame-evaluation-classification EvaluateDataFrameRequest request = new EvaluateDataFrameRequest(indexName, null, evaluation); @@ -3382,16 +3384,34 @@ public void testEvaluateDataFrame_Classification() throws Exception { AccuracyMetric.Result accuracyResult = response.getMetricByName(AccuracyMetric.NAME); // <1> double accuracy = accuracyResult.getOverallAccuracy(); // <2> + org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.Result precisionResult = + response.getMetricByName(org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.NAME); // <3> + double precision = precisionResult.getAvgPrecision(); // <4> + + org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.Result recallResult = + response.getMetricByName(org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.NAME); // <5> + double recall = recallResult.getAvgRecall(); // <6> + MulticlassConfusionMatrixMetric.Result multiclassConfusionMatrix = - response.getMetricByName(MulticlassConfusionMatrixMetric.NAME); // <3> + response.getMetricByName(MulticlassConfusionMatrixMetric.NAME); // <7> - List confusionMatrix = multiclassConfusionMatrix.getConfusionMatrix(); // <4> - long otherClassesCount = multiclassConfusionMatrix.getOtherActualClassCount(); // <5> + List confusionMatrix = multiclassConfusionMatrix.getConfusionMatrix(); // <8> + long otherClassesCount = multiclassConfusionMatrix.getOtherActualClassCount(); // <9> // end::evaluate-data-frame-results-classification assertThat(accuracyResult.getMetricName(), equalTo(AccuracyMetric.NAME)); assertThat(accuracy, equalTo(0.6)); + assertThat( + precisionResult.getMetricName(), + equalTo(org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.NAME)); + assertThat(precision, equalTo(0.675)); + + assertThat( + recallResult.getMetricName(), + equalTo(org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.NAME)); + assertThat(recall, equalTo(0.45)); + assertThat(multiclassConfusionMatrix.getMetricName(), equalTo(MulticlassConfusionMatrixMetric.NAME)); assertThat( confusionMatrix, diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/EvaluateDataFrameResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/EvaluateDataFrameResponseTests.java index f6b7459b1043b..92d3ab81bce47 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/EvaluateDataFrameResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/EvaluateDataFrameResponseTests.java @@ -64,6 +64,8 @@ public static EvaluateDataFrameResponse randomResponse() { metrics = randomSubsetOf( Arrays.asList( AccuracyMetricResultTests.randomResult(), + org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetricResultTests.randomResult(), + org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetricResultTests.randomResult(), MulticlassConfusionMatrixMetricResultTests.randomResult())); break; default: diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/ClassificationTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/ClassificationTests.java index acb6f21cb8209..81691fcbb2eed 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/ClassificationTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/ClassificationTests.java @@ -41,6 +41,8 @@ static Classification createRandom() { randomSubsetOf( Arrays.asList( AccuracyMetricTests.createRandom(), + PrecisionMetricTests.createRandom(), + RecallMetricTests.createRandom(), MulticlassConfusionMatrixMetricTests.createRandom())); return new Classification(randomAlphaOfLength(10), randomAlphaOfLength(10), metrics.isEmpty() ? null : metrics); } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetricResultTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetricResultTests.java new file mode 100644 index 0000000000000..ef6e41e78f0e8 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetricResultTests.java @@ -0,0 +1,67 @@ +/* + * 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. + */ +package org.elasticsearch.client.ml.dataframe.evaluation.classification; + +import org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; +import org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.PerClassResult; +import org.elasticsearch.client.ml.dataframe.evaluation.classification.PrecisionMetric.Result; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class PrecisionMetricResultTests extends AbstractXContentTestCase { + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(new MlEvaluationNamedXContentProvider().getNamedXContentParsers()); + } + + public static Result randomResult() { + int numClasses = randomIntBetween(2, 100); + List classNames = Stream.generate(() -> randomAlphaOfLength(10)).limit(numClasses).collect(Collectors.toList()); + List classes = new ArrayList<>(numClasses); + for (int i = 0; i < numClasses; i++) { + double precision = randomDoubleBetween(0.0, 1.0, true); + classes.add(new PerClassResult(classNames.get(i), precision)); + } + double avgPrecision = randomDoubleBetween(0.0, 1.0, true); + return new Result(classes, avgPrecision); + } + + @Override + protected Result createTestInstance() { + return randomResult(); + } + + @Override + protected Result doParseInstance(XContentParser parser) throws IOException { + return Result.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetricTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetricTests.java new file mode 100644 index 0000000000000..7e21be190d938 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/PrecisionMetricTests.java @@ -0,0 +1,53 @@ +/* + * 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. + */ +package org.elasticsearch.client.ml.dataframe.evaluation.classification; + +import org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class PrecisionMetricTests extends AbstractXContentTestCase { + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(new MlEvaluationNamedXContentProvider().getNamedXContentParsers()); + } + + static PrecisionMetric createRandom() { + return new PrecisionMetric(); + } + + @Override + protected PrecisionMetric createTestInstance() { + return createRandom(); + } + + @Override + protected PrecisionMetric doParseInstance(XContentParser parser) throws IOException { + return PrecisionMetric.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetricResultTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetricResultTests.java new file mode 100644 index 0000000000000..f8fffb405ea1b --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetricResultTests.java @@ -0,0 +1,67 @@ +/* + * 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. + */ +package org.elasticsearch.client.ml.dataframe.evaluation.classification; + +import org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; +import org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.PerClassResult; +import org.elasticsearch.client.ml.dataframe.evaluation.classification.RecallMetric.Result; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class RecallMetricResultTests extends AbstractXContentTestCase { + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(new MlEvaluationNamedXContentProvider().getNamedXContentParsers()); + } + + public static Result randomResult() { + int numClasses = randomIntBetween(2, 100); + List classNames = Stream.generate(() -> randomAlphaOfLength(10)).limit(numClasses).collect(Collectors.toList()); + List classes = new ArrayList<>(numClasses); + for (int i = 0; i < numClasses; i++) { + double recall = randomDoubleBetween(0.0, 1.0, true); + classes.add(new PerClassResult(classNames.get(i), recall)); + } + double avgRecall = randomDoubleBetween(0.0, 1.0, true); + return new Result(classes, avgRecall); + } + + @Override + protected Result createTestInstance() { + return randomResult(); + } + + @Override + protected Result doParseInstance(XContentParser parser) throws IOException { + return Result.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetricTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetricTests.java new file mode 100644 index 0000000000000..087f9838aaf3e --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/RecallMetricTests.java @@ -0,0 +1,53 @@ +/* + * 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. + */ +package org.elasticsearch.client.ml.dataframe.evaluation.classification; + +import org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class RecallMetricTests extends AbstractXContentTestCase { + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(new MlEvaluationNamedXContentProvider().getNamedXContentParsers()); + } + + static RecallMetric createRandom() { + return new RecallMetric(); + } + + @Override + protected RecallMetric createTestInstance() { + return createRandom(); + } + + @Override + protected RecallMetric doParseInstance(XContentParser parser) throws IOException { + return RecallMetric.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/docs/java-rest/high-level/ml/evaluate-data-frame.asciidoc b/docs/java-rest/high-level/ml/evaluate-data-frame.asciidoc index b4abafa249ee0..57a82d1c7132f 100644 --- a/docs/java-rest/high-level/ml/evaluate-data-frame.asciidoc +++ b/docs/java-rest/high-level/ml/evaluate-data-frame.asciidoc @@ -53,7 +53,9 @@ include-tagged::{doc-tests-file}[{api}-evaluation-classification] <3> Name of the field in the index. Its value denotes the predicted (as per some ML algorithm) class of the example. <4> The remaining parameters are the metrics to be calculated based on the two fields described above <5> Accuracy -<6> Multiclass confusion matrix of size 3 +<6> Precision +<7> Recall +<8> Multiclass confusion matrix of size 3 ===== Regression @@ -104,9 +106,13 @@ include-tagged::{doc-tests-file}[{api}-results-classification] <1> Fetching accuracy metric by name <2> Fetching the actual accuracy value -<3> Fetching multiclass confusion matrix metric by name -<4> Fetching the contents of the confusion matrix -<5> Fetching the number of classes that were not included in the matrix +<3> Fetching precision metric by name +<4> Fetching the actual precision value +<5> Fetching recall metric by name +<6> Fetching the actual recall value +<7> Fetching multiclass confusion matrix metric by name +<8> Fetching the contents of the confusion matrix +<9> Fetching the number of classes that were not included in the matrix ===== Regression @@ -118,4 +124,4 @@ include-tagged::{doc-tests-file}[{api}-results-regression] <1> Fetching mean squared error metric by name <2> Fetching the actual mean squared error value <3> Fetching R squared metric by name -<4> Fetching the actual R squared value \ No newline at end of file +<4> Fetching the actual R squared value diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java index 9128f75a0937b..06bc4182cabfd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java @@ -68,7 +68,6 @@ import org.elasticsearch.xpack.core.ml.MlMetadata; import org.elasticsearch.xpack.core.ml.MlTasks; import org.elasticsearch.xpack.core.ml.action.CloseJobAction; -import org.elasticsearch.xpack.core.ml.action.ExplainDataFrameAnalyticsAction; import org.elasticsearch.xpack.core.ml.action.DeleteCalendarAction; import org.elasticsearch.xpack.core.ml.action.DeleteCalendarEventAction; import org.elasticsearch.xpack.core.ml.action.DeleteDataFrameAnalyticsAction; @@ -80,6 +79,7 @@ import org.elasticsearch.xpack.core.ml.action.DeleteModelSnapshotAction; import org.elasticsearch.xpack.core.ml.action.DeleteTrainedModelAction; import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction; +import org.elasticsearch.xpack.core.ml.action.ExplainDataFrameAnalyticsAction; import org.elasticsearch.xpack.core.ml.action.FinalizeJobExecutionAction; import org.elasticsearch.xpack.core.ml.action.FindFileStructureAction; import org.elasticsearch.xpack.core.ml.action.FlushJobAction; @@ -134,15 +134,7 @@ import org.elasticsearch.xpack.core.ml.dataframe.analyses.DataFrameAnalysis; import org.elasticsearch.xpack.core.ml.dataframe.analyses.OutlierDetection; import org.elasticsearch.xpack.core.ml.dataframe.analyses.Regression; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.Evaluation; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.AucRoc; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.BinarySoftClassification; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.ConfusionMatrix; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.Precision; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.Recall; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.ScoreByThresholdResult; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.SoftClassificationMetric; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; import org.elasticsearch.xpack.core.ml.inference.preprocessing.FrequencyEncoding; import org.elasticsearch.xpack.core.ml.inference.preprocessing.OneHotEncoding; import org.elasticsearch.xpack.core.ml.inference.preprocessing.PreProcessor; @@ -245,6 +237,9 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toList; // TODO: merge this into XPackPlugin public class XPackClientPlugin extends Plugin implements ActionPlugin, NetworkPlugin { @@ -426,7 +421,8 @@ public List> getClientActions() { @Override public List getNamedWriteables() { - return Arrays.asList( + return Stream.concat( + Arrays.asList( // graph new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.GRAPH, GraphFeatureSetUsage::new), // logstash @@ -454,18 +450,6 @@ public List getNamedWriteables() { new NamedWriteableRegistry.Entry(DataFrameAnalysis.class, OutlierDetection.NAME.getPreferredName(), OutlierDetection::new), new NamedWriteableRegistry.Entry(DataFrameAnalysis.class, Regression.NAME.getPreferredName(), Regression::new), new NamedWriteableRegistry.Entry(DataFrameAnalysis.class, Classification.NAME.getPreferredName(), Classification::new), - // ML - Data frame evaluation - new NamedWriteableRegistry.Entry(Evaluation.class, BinarySoftClassification.NAME.getPreferredName(), - BinarySoftClassification::new), - new NamedWriteableRegistry.Entry(SoftClassificationMetric.class, AucRoc.NAME.getPreferredName(), AucRoc::new), - new NamedWriteableRegistry.Entry(SoftClassificationMetric.class, Precision.NAME.getPreferredName(), Precision::new), - new NamedWriteableRegistry.Entry(SoftClassificationMetric.class, Recall.NAME.getPreferredName(), Recall::new), - new NamedWriteableRegistry.Entry(SoftClassificationMetric.class, ConfusionMatrix.NAME.getPreferredName(), - ConfusionMatrix::new), - new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, AucRoc.NAME.getPreferredName(), AucRoc.Result::new), - new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, ScoreByThresholdResult.NAME, ScoreByThresholdResult::new), - new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, ConfusionMatrix.NAME.getPreferredName(), - ConfusionMatrix.Result::new), // ML - Inference preprocessing new NamedWriteableRegistry.Entry(PreProcessor.class, FrequencyEncoding.NAME.getPreferredName(), FrequencyEncoding::new), new NamedWriteableRegistry.Entry(PreProcessor.class, OneHotEncoding.NAME.getPreferredName(), OneHotEncoding::new), @@ -568,7 +552,9 @@ public List getNamedWriteables() { new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.SPATIAL, SpatialFeatureSetUsage::new), // data science new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.ANALYTICS, AnalyticsFeatureSetUsage::new) - ); + ).stream(), + MlEvaluationNamedXContentProvider.getNamedWriteables().stream() + ).collect(toList()); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/Evaluation.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/Evaluation.java index 98888c539c189..1a79dff41e10c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/Evaluation.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/Evaluation.java @@ -7,12 +7,14 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; @@ -76,8 +78,9 @@ default SearchSourceBuilder buildSearch(QueryBuilder userProvidedQueryBuilder) { SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().size(0).query(boolQuery); for (EvaluationMetric metric : getMetrics()) { // Fetch aggregations requested by individual metrics - List aggs = metric.aggs(getActualField(), getPredictedField()); - aggs.forEach(searchSourceBuilder::aggregation); + Tuple, List> aggs = metric.aggs(getActualField(), getPredictedField()); + aggs.v1().forEach(searchSourceBuilder::aggregation); + aggs.v2().forEach(searchSourceBuilder::aggregation); } return searchSourceBuilder; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java index 7a539d030dd44..36bf7634cb43f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java @@ -6,10 +6,12 @@ package org.elasticsearch.xpack.core.ml.dataframe.evaluation; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import java.util.List; import java.util.Optional; @@ -30,7 +32,7 @@ public interface EvaluationMetric extends ToXContentObject, NamedWriteable { * @param predictedField the field that stores the predicted value (class name or probability) * @return the aggregations required to compute the metric */ - List aggs(String actualField, String predictedField); + Tuple, List> aggs(String actualField, String predictedField); /** * Processes given aggregations as a step towards computing result diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MlEvaluationNamedXContentProvider.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MlEvaluationNamedXContentProvider.java index 1ef8b89a99609..42e530a7a602d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MlEvaluationNamedXContentProvider.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MlEvaluationNamedXContentProvider.java @@ -5,109 +5,179 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.evaluation; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.plugins.spi.NamedXContentProvider; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Classification; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.ClassificationMetric; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.regression.MeanSquaredError; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.regression.MeanSquaredError; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.regression.RSquared; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.regression.Regression; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.regression.RegressionMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.AucRoc; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.BinarySoftClassification; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.ConfusionMatrix; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.Precision; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.Recall; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.ScoreByThresholdResult; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.SoftClassificationMetric; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class MlEvaluationNamedXContentProvider implements NamedXContentProvider { - @Override - public List getNamedXContentParsers() { - List namedXContent = new ArrayList<>(); + /** + * Constructs the name under which a metric (or metric result) is registered. + * The name is prefixed with evaluation name so that registered names are unique. + * + * @param evaluationName name of the evaluation + * @param metricName name of the metric + * @return name appropriate for registering a metric (or metric result) in {@link NamedXContentRegistry} + */ + public static String registeredMetricName(ParseField evaluationName, ParseField metricName) { + return registeredMetricName(evaluationName.getPreferredName(), metricName.getPreferredName()); + } - // Evaluations - namedXContent.add(new NamedXContentRegistry.Entry(Evaluation.class, BinarySoftClassification.NAME, - BinarySoftClassification::fromXContent)); - namedXContent.add(new NamedXContentRegistry.Entry(Evaluation.class, Classification.NAME, Classification::fromXContent)); - namedXContent.add(new NamedXContentRegistry.Entry(Evaluation.class, Regression.NAME, Regression::fromXContent)); + /** + * Constructs the name under which a metric (or metric result) is registered. + * The name is prefixed with evaluation name so that registered names are unique. + * + * @param evaluationName name of the evaluation + * @param metricName name of the metric + * @return name appropriate for registering a metric (or metric result) in {@link NamedXContentRegistry} + */ + public static String registeredMetricName(String evaluationName, String metricName) { + return evaluationName + "." + metricName; + } - // Soft classification metrics - namedXContent.add(new NamedXContentRegistry.Entry(SoftClassificationMetric.class, AucRoc.NAME, AucRoc::fromXContent)); - namedXContent.add(new NamedXContentRegistry.Entry(SoftClassificationMetric.class, Precision.NAME, Precision::fromXContent)); - namedXContent.add(new NamedXContentRegistry.Entry(SoftClassificationMetric.class, Recall.NAME, Recall::fromXContent)); - namedXContent.add(new NamedXContentRegistry.Entry(SoftClassificationMetric.class, ConfusionMatrix.NAME, - ConfusionMatrix::fromXContent)); + @Override + public List getNamedXContentParsers() { + return Arrays.asList( + // Evaluations + new NamedXContentRegistry.Entry(Evaluation.class, BinarySoftClassification.NAME, BinarySoftClassification::fromXContent), + new NamedXContentRegistry.Entry(Evaluation.class, Classification.NAME, Classification::fromXContent), + new NamedXContentRegistry.Entry(Evaluation.class, Regression.NAME, Regression::fromXContent), - // Classification metrics - namedXContent.add(new NamedXContentRegistry.Entry(ClassificationMetric.class, MulticlassConfusionMatrix.NAME, - MulticlassConfusionMatrix::fromXContent)); - namedXContent.add(new NamedXContentRegistry.Entry(ClassificationMetric.class, Accuracy.NAME, Accuracy::fromXContent)); + // Soft classification metrics + new NamedXContentRegistry.Entry(EvaluationMetric.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, AucRoc.NAME)), + AucRoc::fromXContent), + new NamedXContentRegistry.Entry(EvaluationMetric.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, Precision.NAME)), + Precision::fromXContent), + new NamedXContentRegistry.Entry(EvaluationMetric.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, Recall.NAME)), + Recall::fromXContent), + new NamedXContentRegistry.Entry(EvaluationMetric.class, + new ParseField(registeredMetricName(BinarySoftClassification.NAME, ConfusionMatrix.NAME)), + ConfusionMatrix::fromXContent), - // Regression metrics - namedXContent.add(new NamedXContentRegistry.Entry(RegressionMetric.class, MeanSquaredError.NAME, MeanSquaredError::fromXContent)); - namedXContent.add(new NamedXContentRegistry.Entry(RegressionMetric.class, RSquared.NAME, RSquared::fromXContent)); + // Classification metrics + new NamedXContentRegistry.Entry(EvaluationMetric.class, + new ParseField(registeredMetricName(Classification.NAME, MulticlassConfusionMatrix.NAME)), + MulticlassConfusionMatrix::fromXContent), + new NamedXContentRegistry.Entry(EvaluationMetric.class, + new ParseField(registeredMetricName(Classification.NAME, Accuracy.NAME)), + Accuracy::fromXContent), + new NamedXContentRegistry.Entry(EvaluationMetric.class, + new ParseField( + registeredMetricName( + Classification.NAME, org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision.NAME)), + org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision::fromXContent), + new NamedXContentRegistry.Entry(EvaluationMetric.class, + new ParseField( + registeredMetricName( + Classification.NAME, org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall.NAME)), + org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall::fromXContent), - return namedXContent; + // Regression metrics + new NamedXContentRegistry.Entry(EvaluationMetric.class, + new ParseField(registeredMetricName(Regression.NAME, MeanSquaredError.NAME)), + MeanSquaredError::fromXContent), + new NamedXContentRegistry.Entry(EvaluationMetric.class, + new ParseField(registeredMetricName(Regression.NAME, RSquared.NAME)), + RSquared::fromXContent) + ); } - public List getNamedWriteables() { - List namedWriteables = new ArrayList<>(); - - // Evaluations - namedWriteables.add(new NamedWriteableRegistry.Entry(Evaluation.class, BinarySoftClassification.NAME.getPreferredName(), - BinarySoftClassification::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(Evaluation.class, Classification.NAME.getPreferredName(), - Classification::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(Evaluation.class, Regression.NAME.getPreferredName(), Regression::new)); - - // Evaluation Metrics - namedWriteables.add(new NamedWriteableRegistry.Entry(SoftClassificationMetric.class, AucRoc.NAME.getPreferredName(), - AucRoc::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(SoftClassificationMetric.class, Precision.NAME.getPreferredName(), - Precision::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(SoftClassificationMetric.class, Recall.NAME.getPreferredName(), - Recall::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(SoftClassificationMetric.class, ConfusionMatrix.NAME.getPreferredName(), - ConfusionMatrix::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(ClassificationMetric.class, - MulticlassConfusionMatrix.NAME.getPreferredName(), - MulticlassConfusionMatrix::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(ClassificationMetric.class, Accuracy.NAME.getPreferredName(), Accuracy::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(RegressionMetric.class, - MeanSquaredError.NAME.getPreferredName(), - MeanSquaredError::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(RegressionMetric.class, - RSquared.NAME.getPreferredName(), - RSquared::new)); + public static List getNamedWriteables() { + return Arrays.asList( + // Evaluations + new NamedWriteableRegistry.Entry(Evaluation.class, + BinarySoftClassification.NAME.getPreferredName(), + BinarySoftClassification::new), + new NamedWriteableRegistry.Entry(Evaluation.class, + Classification.NAME.getPreferredName(), + Classification::new), + new NamedWriteableRegistry.Entry(Evaluation.class, + Regression.NAME.getPreferredName(), + Regression::new), - // Evaluation Metrics Results - namedWriteables.add(new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, AucRoc.NAME.getPreferredName(), - AucRoc.Result::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, ScoreByThresholdResult.NAME, - ScoreByThresholdResult::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, ConfusionMatrix.NAME.getPreferredName(), - ConfusionMatrix.Result::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, - MulticlassConfusionMatrix.NAME.getPreferredName(), - MulticlassConfusionMatrix.Result::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, - Accuracy.NAME.getPreferredName(), - Accuracy.Result::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, - MeanSquaredError.NAME.getPreferredName(), - MeanSquaredError.Result::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, - RSquared.NAME.getPreferredName(), - RSquared.Result::new)); + // Evaluation metrics + new NamedWriteableRegistry.Entry(EvaluationMetric.class, + registeredMetricName(BinarySoftClassification.NAME, AucRoc.NAME), + AucRoc::new), + new NamedWriteableRegistry.Entry(EvaluationMetric.class, + registeredMetricName(BinarySoftClassification.NAME, Precision.NAME), + Precision::new), + new NamedWriteableRegistry.Entry(EvaluationMetric.class, + registeredMetricName(BinarySoftClassification.NAME, Recall.NAME), + Recall::new), + new NamedWriteableRegistry.Entry(EvaluationMetric.class, + registeredMetricName(BinarySoftClassification.NAME, ConfusionMatrix.NAME), + ConfusionMatrix::new), + new NamedWriteableRegistry.Entry(EvaluationMetric.class, + registeredMetricName(Classification.NAME, MulticlassConfusionMatrix.NAME), + MulticlassConfusionMatrix::new), + new NamedWriteableRegistry.Entry(EvaluationMetric.class, + registeredMetricName(Classification.NAME, Accuracy.NAME), + Accuracy::new), + new NamedWriteableRegistry.Entry(EvaluationMetric.class, + registeredMetricName( + Classification.NAME, org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision.NAME), + org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision::new), + new NamedWriteableRegistry.Entry(EvaluationMetric.class, + registeredMetricName( + Classification.NAME, org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall.NAME), + org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall::new), + new NamedWriteableRegistry.Entry(EvaluationMetric.class, + registeredMetricName(Regression.NAME, MeanSquaredError.NAME), + MeanSquaredError::new), + new NamedWriteableRegistry.Entry(EvaluationMetric.class, + registeredMetricName(Regression.NAME, RSquared.NAME), + RSquared::new), - return namedWriteables; + // Evaluation metrics results + new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, + registeredMetricName(BinarySoftClassification.NAME, AucRoc.NAME), + AucRoc.Result::new), + new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, + registeredMetricName(BinarySoftClassification.NAME, ScoreByThresholdResult.NAME), + ScoreByThresholdResult::new), + new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, + registeredMetricName(BinarySoftClassification.NAME, ConfusionMatrix.NAME), + ConfusionMatrix.Result::new), + new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, + registeredMetricName(Classification.NAME, MulticlassConfusionMatrix.NAME), + MulticlassConfusionMatrix.Result::new), + new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, + registeredMetricName(Classification.NAME, Accuracy.NAME), + Accuracy.Result::new), + new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, + registeredMetricName( + Classification.NAME, org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision.NAME), + org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision.Result::new), + new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, + registeredMetricName( + Classification.NAME, org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall.NAME), + org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall.Result::new), + new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, + registeredMetricName(Regression.NAME, MeanSquaredError.NAME), + MeanSquaredError.Result::new), + new NamedWriteableRegistry.Entry(EvaluationMetricResult.class, + registeredMetricName(Regression.NAME, RSquared.NAME), + RSquared.Result::new) + ); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java index d0db9189b0422..8e7b8b6066932 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -18,8 +19,10 @@ import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; @@ -33,6 +36,7 @@ import java.util.Optional; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; /** * {@link Accuracy} is a metric that answers the question: @@ -40,7 +44,7 @@ * * equation: accuracy = 1/n * Σ(y == y´) */ -public class Accuracy implements ClassificationMetric { +public class Accuracy implements EvaluationMetric { public static final ParseField NAME = new ParseField("accuracy"); @@ -67,7 +71,7 @@ public Accuracy(StreamInput in) throws IOException {} @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(Classification.NAME, NAME); } @Override @@ -76,16 +80,18 @@ public String getName() { } @Override - public final List aggs(String actualField, String predictedField) { + public final Tuple, List> aggs(String actualField, String predictedField) { if (result != null) { - return List.of(); + return Tuple.tuple(List.of(), List.of()); } Script accuracyScript = new Script(buildScript(actualField, predictedField)); - return List.of( - AggregationBuilders.terms(CLASSES_AGG_NAME) - .field(actualField) - .subAggregation(AggregationBuilders.avg(PER_CLASS_ACCURACY_AGG_NAME).script(accuracyScript)), - AggregationBuilders.avg(OVERALL_ACCURACY_AGG_NAME).script(accuracyScript)); + return Tuple.tuple( + List.of( + AggregationBuilders.terms(CLASSES_AGG_NAME) + .field(actualField) + .subAggregation(AggregationBuilders.avg(PER_CLASS_ACCURACY_AGG_NAME).script(accuracyScript)), + AggregationBuilders.avg(OVERALL_ACCURACY_AGG_NAME).script(accuracyScript)), + List.of()); } @Override @@ -168,7 +174,7 @@ public Result(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(Classification.NAME, NAME); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Classification.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Classification.java index ee312ee7c7fd8..fb8014697555e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Classification.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Classification.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.Evaluation; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.io.IOException; @@ -20,6 +21,8 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + /** * Evaluation of classification results. */ @@ -33,13 +36,13 @@ public class Classification implements Evaluation { @SuppressWarnings("unchecked") public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - NAME.getPreferredName(), a -> new Classification((String) a[0], (String) a[1], (List) a[2])); + NAME.getPreferredName(), a -> new Classification((String) a[0], (String) a[1], (List) a[2])); static { PARSER.declareString(ConstructingObjectParser.constructorArg(), ACTUAL_FIELD); PARSER.declareString(ConstructingObjectParser.constructorArg(), PREDICTED_FIELD); PARSER.declareNamedObjects(ConstructingObjectParser.optionalConstructorArg(), - (p, c, n) -> p.namedObject(ClassificationMetric.class, n, c), METRICS); + (p, c, n) -> p.namedObject(EvaluationMetric.class, registeredMetricName(NAME.getPreferredName(), n), c), METRICS); } public static Classification fromXContent(XContentParser parser) { @@ -61,22 +64,22 @@ public static Classification fromXContent(XContentParser parser) { /** * The list of metrics to calculate */ - private final List metrics; + private final List metrics; - public Classification(String actualField, String predictedField, @Nullable List metrics) { + public Classification(String actualField, String predictedField, @Nullable List metrics) { this.actualField = ExceptionsHelper.requireNonNull(actualField, ACTUAL_FIELD); this.predictedField = ExceptionsHelper.requireNonNull(predictedField, PREDICTED_FIELD); this.metrics = initMetrics(metrics, Classification::defaultMetrics); } - private static List defaultMetrics() { + private static List defaultMetrics() { return Arrays.asList(new MulticlassConfusionMatrix()); } public Classification(StreamInput in) throws IOException { this.actualField = in.readString(); this.predictedField = in.readString(); - this.metrics = in.readNamedWriteableList(ClassificationMetric.class); + this.metrics = in.readNamedWriteableList(EvaluationMetric.class); } @Override @@ -95,7 +98,7 @@ public String getPredictedField() { } @Override - public List getMetrics() { + public List getMetrics() { return metrics; } @@ -118,8 +121,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(PREDICTED_FIELD.getPreferredName(), predictedField); builder.startObject(METRICS.getPreferredName()); - for (ClassificationMetric metric : metrics) { - builder.field(metric.getWriteableName(), metric); + for (EvaluationMetric metric : metrics) { + builder.field(metric.getName(), metric); } builder.endObject(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationMetric.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationMetric.java deleted file mode 100644 index a61ac9a702fa2..0000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationMetric.java +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; - -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; - -public interface ClassificationMetric extends EvaluationMetric { -} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java index 1a53b48666a5a..7b9d524abf6f7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java @@ -7,6 +7,7 @@ import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -19,10 +20,12 @@ import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.BucketOrder; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.filter.Filters; import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator.KeyedFilter; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.metrics.Cardinality; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; @@ -37,13 +40,14 @@ import static java.util.Comparator.comparing; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; /** * {@link MulticlassConfusionMatrix} is a metric that answers the question: - * "How many examples belonging to class X were classified as Y by the classifier?" + * "How many documents belonging to class X were classified as Y by the classifier?" * for all the possible class pairs {X, Y}. */ -public class MulticlassConfusionMatrix implements ClassificationMetric { +public class MulticlassConfusionMatrix implements EvaluationMetric { public static final ParseField NAME = new ParseField("multiclass_confusion_matrix"); @@ -91,7 +95,7 @@ public MulticlassConfusionMatrix(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(Classification.NAME, NAME); } @Override @@ -104,13 +108,15 @@ public int getSize() { } @Override - public final List aggs(String actualField, String predictedField) { + public final Tuple, List> aggs(String actualField, String predictedField) { if (topActualClassNames == null) { // This is step 1 - return List.of( - AggregationBuilders.terms(STEP_1_AGGREGATE_BY_ACTUAL_CLASS) - .field(actualField) - .order(List.of(BucketOrder.count(false), BucketOrder.key(true))) - .size(size)); + return Tuple.tuple( + List.of( + AggregationBuilders.terms(STEP_1_AGGREGATE_BY_ACTUAL_CLASS) + .field(actualField) + .order(List.of(BucketOrder.count(false), BucketOrder.key(true))) + .size(size)), + List.of()); } if (result == null) { // This is step 2 KeyedFilter[] keyedFiltersActual = @@ -121,15 +127,17 @@ public final List aggs(String actualField, String predictedF topActualClassNames.stream() .map(className -> new KeyedFilter(className, QueryBuilders.termQuery(predictedField, className))) .toArray(KeyedFilter[]::new); - return List.of( - AggregationBuilders.cardinality(STEP_2_CARDINALITY_OF_ACTUAL_CLASS) - .field(actualField), - AggregationBuilders.filters(STEP_2_AGGREGATE_BY_ACTUAL_CLASS, keyedFiltersActual) - .subAggregation(AggregationBuilders.filters(STEP_2_AGGREGATE_BY_PREDICTED_CLASS, keyedFiltersPredicted) - .otherBucket(true) - .otherBucketKey(OTHER_BUCKET_KEY))); - } - return List.of(); + return Tuple.tuple( + List.of( + AggregationBuilders.cardinality(STEP_2_CARDINALITY_OF_ACTUAL_CLASS) + .field(actualField), + AggregationBuilders.filters(STEP_2_AGGREGATE_BY_ACTUAL_CLASS, keyedFiltersActual) + .subAggregation(AggregationBuilders.filters(STEP_2_AGGREGATE_BY_PREDICTED_CLASS, keyedFiltersPredicted) + .otherBucket(true) + .otherBucketKey(OTHER_BUCKET_KEY))), + List.of()); + } + return Tuple.tuple(List.of(), List.of()); } @Override @@ -231,7 +239,7 @@ public Result(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(Classification.NAME, NAME); } @Override @@ -300,7 +308,7 @@ public static class ActualClass implements ToXContentObject, Writeable { /** Name of the actual class. */ private final String actualClass; - /** Number of documents (examples) belonging to the {code actualClass} class. */ + /** Number of documents belonging to the {code actualClass} class. */ private final long actualClassDocCount; /** List of predicted classes. */ private final List predictedClasses; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java new file mode 100644 index 0000000000000..f6bacbec05e7b --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java @@ -0,0 +1,345 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.script.Script; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilders; +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.BucketOrder; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; +import org.elasticsearch.search.aggregations.PipelineAggregatorBuilders; +import org.elasticsearch.search.aggregations.bucket.filter.Filters; +import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator.KeyedFilter; +import org.elasticsearch.search.aggregations.bucket.terms.Terms; +import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; +import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + +/** + * {@link Precision} is a metric that answers the question: + * "What fraction of documents classified as X actually belongs to X?" + * for any given class X + * + * equation: precision(X) = TP(X) / (TP(X) + FP(X)) + * where: TP(X) - number of true positives wrt X + * FP(X) - number of false positives wrt X + */ +public class Precision implements EvaluationMetric { + + public static final ParseField NAME = new ParseField("precision"); + + private static final String PAINLESS_TEMPLATE = "doc[''{0}''].value == doc[''{1}''].value"; + private static final String AGG_NAME_PREFIX = "classification_precision_"; + static final String ACTUAL_CLASSES_NAMES_AGG_NAME = AGG_NAME_PREFIX + "by_actual_class"; + static final String BY_PREDICTED_CLASS_AGG_NAME = AGG_NAME_PREFIX + "by_predicted_class"; + static final String PER_PREDICTED_CLASS_PRECISION_AGG_NAME = AGG_NAME_PREFIX + "per_predicted_class_precision"; + static final String AVG_PRECISION_AGG_NAME = AGG_NAME_PREFIX + "avg_precision"; + + private static Script buildScript(Object...args) { + return new Script(new MessageFormat(PAINLESS_TEMPLATE, Locale.ROOT).format(args)); + } + + private static final ObjectParser PARSER = new ObjectParser<>(NAME.getPreferredName(), true, Precision::new); + + public static Precision fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + private static final int DEFAULT_MAX_CLASSES_CARDINALITY = 1000; + + private final int maxClassesCardinality; + private String actualField; + private List topActualClassNames; + private EvaluationMetricResult result; + + public Precision() { + this((Integer) null); + } + + // Visible for testing + public Precision(@Nullable Integer maxClassesCardinality) { + this.maxClassesCardinality = maxClassesCardinality != null ? maxClassesCardinality : DEFAULT_MAX_CLASSES_CARDINALITY; + } + + public Precision(StreamInput in) throws IOException { + this.maxClassesCardinality = DEFAULT_MAX_CLASSES_CARDINALITY; + } + + @Override + public String getWriteableName() { + return registeredMetricName(Classification.NAME, NAME); + } + + @Override + public String getName() { + return NAME.getPreferredName(); + } + + @Override + public final Tuple, List> aggs(String actualField, String predictedField) { + // Store given {@code actualField} for the purpose of generating error message in {@code process}. + this.actualField = actualField; + if (topActualClassNames == null) { // This is step 1 + return Tuple.tuple( + List.of( + AggregationBuilders.terms(ACTUAL_CLASSES_NAMES_AGG_NAME) + .field(actualField) + .order(List.of(BucketOrder.count(false), BucketOrder.key(true))) + .size(maxClassesCardinality)), + List.of()); + } + if (result == null) { // This is step 2 + KeyedFilter[] keyedFiltersPredicted = + topActualClassNames.stream() + .map(className -> new KeyedFilter(className, QueryBuilders.termQuery(predictedField, className))) + .toArray(KeyedFilter[]::new); + Script script = buildScript(actualField, predictedField); + return Tuple.tuple( + List.of( + AggregationBuilders.filters(BY_PREDICTED_CLASS_AGG_NAME, keyedFiltersPredicted) + .subAggregation(AggregationBuilders.avg(PER_PREDICTED_CLASS_PRECISION_AGG_NAME).script(script))), + List.of( + PipelineAggregatorBuilders.avgBucket( + AVG_PRECISION_AGG_NAME, + BY_PREDICTED_CLASS_AGG_NAME + ">" + PER_PREDICTED_CLASS_PRECISION_AGG_NAME))); + } + return Tuple.tuple(List.of(), List.of()); + } + + @Override + public void process(Aggregations aggs) { + if (topActualClassNames == null && aggs.get(ACTUAL_CLASSES_NAMES_AGG_NAME) instanceof Terms) { + Terms topActualClassesAgg = aggs.get(ACTUAL_CLASSES_NAMES_AGG_NAME); + if (topActualClassesAgg.getSumOfOtherDocCounts() > 0) { + // This means there were more than {@code maxClassesCardinality} buckets. + // We cannot calculate average precision accurately, so we fail. + throw ExceptionsHelper.badRequestException( + "Cannot calculate average precision. Cardinality of field [{}] is too high", actualField); + } + topActualClassNames = + topActualClassesAgg.getBuckets().stream().map(Terms.Bucket::getKeyAsString).sorted().collect(Collectors.toList()); + } + if (result == null && + aggs.get(BY_PREDICTED_CLASS_AGG_NAME) instanceof Filters && + aggs.get(AVG_PRECISION_AGG_NAME) instanceof NumericMetricsAggregation.SingleValue) { + Filters byPredictedClassAgg = aggs.get(BY_PREDICTED_CLASS_AGG_NAME); + NumericMetricsAggregation.SingleValue avgPrecisionAgg = aggs.get(AVG_PRECISION_AGG_NAME); + List classes = new ArrayList<>(byPredictedClassAgg.getBuckets().size()); + for (Filters.Bucket bucket : byPredictedClassAgg.getBuckets()) { + String className = bucket.getKeyAsString(); + NumericMetricsAggregation.SingleValue precisionAgg = bucket.getAggregations().get(PER_PREDICTED_CLASS_PRECISION_AGG_NAME); + double precision = precisionAgg.value(); + if (Double.isFinite(precision)) { + classes.add(new PerClassResult(className, precision)); + } + } + result = new Result(classes, avgPrecisionAgg.value()); + } + } + + @Override + public Optional getResult() { + return Optional.ofNullable(result); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return true; + } + + @Override + public int hashCode() { + return Objects.hashCode(NAME.getPreferredName()); + } + + public static class Result implements EvaluationMetricResult { + + private static final ParseField CLASSES = new ParseField("classes"); + private static final ParseField AVG_PRECISION = new ParseField("avg_precision"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("precision_result", true, a -> new Result((List) a[0], (double) a[1])); + + static { + PARSER.declareObjectArray(constructorArg(), PerClassResult.PARSER, CLASSES); + PARSER.declareDouble(constructorArg(), AVG_PRECISION); + } + + public static Result fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + /** List of per-class results. */ + private final List classes; + /** Average of per-class precisions. */ + private final double avgPrecision; + + public Result(List classes, double avgPrecision) { + this.classes = Collections.unmodifiableList(ExceptionsHelper.requireNonNull(classes, CLASSES)); + this.avgPrecision = avgPrecision; + } + + public Result(StreamInput in) throws IOException { + this.classes = Collections.unmodifiableList(in.readList(PerClassResult::new)); + this.avgPrecision = in.readDouble(); + } + + @Override + public String getWriteableName() { + return registeredMetricName(Classification.NAME, NAME); + } + + @Override + public String getMetricName() { + return NAME.getPreferredName(); + } + + public List getClasses() { + return classes; + } + + public double getAvgPrecision() { + return avgPrecision; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeList(classes); + out.writeDouble(avgPrecision); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(CLASSES.getPreferredName(), classes); + builder.field(AVG_PRECISION.getPreferredName(), avgPrecision); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Result that = (Result) o; + return Objects.equals(this.classes, that.classes) + && this.avgPrecision == that.avgPrecision; + } + + @Override + public int hashCode() { + return Objects.hash(classes, avgPrecision); + } + } + + public static class PerClassResult implements ToXContentObject, Writeable { + + private static final ParseField CLASS_NAME = new ParseField("class_name"); + private static final ParseField PRECISION = new ParseField("precision"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("precision_per_class_result", true, a -> new PerClassResult((String) a[0], (double) a[1])); + + static { + PARSER.declareString(constructorArg(), CLASS_NAME); + PARSER.declareDouble(constructorArg(), PRECISION); + } + + /** Name of the class. */ + private final String className; + /** Fraction of documents predicted as belonging to the {@code predictedClass} class predicted correctly. */ + private final double precision; + + public PerClassResult(String className, double precision) { + this.className = ExceptionsHelper.requireNonNull(className, CLASS_NAME); + this.precision = precision; + } + + public PerClassResult(StreamInput in) throws IOException { + this.className = in.readString(); + this.precision = in.readDouble(); + } + + public String getClassName() { + return className; + } + + public double getPrecision() { + return precision; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(className); + out.writeDouble(precision); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(CLASS_NAME.getPreferredName(), className); + builder.field(PRECISION.getPreferredName(), precision); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PerClassResult that = (PerClassResult) o; + return Objects.equals(this.className, that.className) + && this.precision == that.precision; + } + + @Override + public int hashCode() { + return Objects.hash(className, precision); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java new file mode 100644 index 0000000000000..522810e57e2dd --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.Script; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilders; +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; +import org.elasticsearch.search.aggregations.PipelineAggregatorBuilders; +import org.elasticsearch.search.aggregations.bucket.terms.Terms; +import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; +import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + +/** + * {@link Recall} is a metric that answers the question: + * "What fraction of documents belonging to X have been predicted as X by the classifier?" + * for any given class X + * + * equation: recall(X) = TP(X) / (TP(X) + FN(X)) + * where: TP(X) - number of true positives wrt X + * FN(X) - number of false negatives wrt X + */ +public class Recall implements EvaluationMetric { + + public static final ParseField NAME = new ParseField("recall"); + + private static final String PAINLESS_TEMPLATE = "doc[''{0}''].value == doc[''{1}''].value"; + private static final String AGG_NAME_PREFIX = "classification_recall_"; + static final String BY_ACTUAL_CLASS_AGG_NAME = AGG_NAME_PREFIX + "by_actual_class"; + static final String PER_ACTUAL_CLASS_RECALL_AGG_NAME = AGG_NAME_PREFIX + "per_actual_class_recall"; + static final String AVG_RECALL_AGG_NAME = AGG_NAME_PREFIX + "avg_recall"; + + private static Script buildScript(Object...args) { + return new Script(new MessageFormat(PAINLESS_TEMPLATE, Locale.ROOT).format(args)); + } + + private static final ObjectParser PARSER = new ObjectParser<>(NAME.getPreferredName(), true, Recall::new); + + public static Recall fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + private static final int DEFAULT_MAX_CLASSES_CARDINALITY = 1000; + + private final int maxClassesCardinality; + private String actualField; + private EvaluationMetricResult result; + + public Recall() { + this((Integer) null); + } + + // Visible for testing + public Recall(@Nullable Integer maxClassesCardinality) { + this.maxClassesCardinality = maxClassesCardinality != null ? maxClassesCardinality : DEFAULT_MAX_CLASSES_CARDINALITY; + } + + public Recall(StreamInput in) throws IOException { + this.maxClassesCardinality = DEFAULT_MAX_CLASSES_CARDINALITY; + } + + @Override + public String getWriteableName() { + return registeredMetricName(Classification.NAME, NAME); + } + + @Override + public String getName() { + return NAME.getPreferredName(); + } + + @Override + public final Tuple, List> aggs(String actualField, String predictedField) { + // Store given {@code actualField} for the purpose of generating error message in {@code process}. + this.actualField = actualField; + if (result != null) { + return Tuple.tuple(List.of(), List.of()); + } + Script script = buildScript(actualField, predictedField); + return Tuple.tuple( + List.of( + AggregationBuilders.terms(BY_ACTUAL_CLASS_AGG_NAME) + .field(actualField) + .size(maxClassesCardinality) + .subAggregation(AggregationBuilders.avg(PER_ACTUAL_CLASS_RECALL_AGG_NAME).script(script))), + List.of( + PipelineAggregatorBuilders.avgBucket( + AVG_RECALL_AGG_NAME, + BY_ACTUAL_CLASS_AGG_NAME + ">" + PER_ACTUAL_CLASS_RECALL_AGG_NAME))); + } + + @Override + public void process(Aggregations aggs) { + if (result == null && + aggs.get(BY_ACTUAL_CLASS_AGG_NAME) instanceof Terms && + aggs.get(AVG_RECALL_AGG_NAME) instanceof NumericMetricsAggregation.SingleValue) { + Terms byActualClassAgg = aggs.get(BY_ACTUAL_CLASS_AGG_NAME); + if (byActualClassAgg.getSumOfOtherDocCounts() > 0) { + // This means there were more than {@code maxClassesCardinality} buckets. + // We cannot calculate average recall accurately, so we fail. + throw ExceptionsHelper.badRequestException( + "Cannot calculate average recall. Cardinality of field [{}] is too high", actualField); + } + NumericMetricsAggregation.SingleValue avgRecallAgg = aggs.get(AVG_RECALL_AGG_NAME); + List classes = new ArrayList<>(byActualClassAgg.getBuckets().size()); + for (Terms.Bucket bucket : byActualClassAgg.getBuckets()) { + String className = bucket.getKeyAsString(); + NumericMetricsAggregation.SingleValue recallAgg = bucket.getAggregations().get(PER_ACTUAL_CLASS_RECALL_AGG_NAME); + classes.add(new PerClassResult(className, recallAgg.value())); + } + result = new Result(classes, avgRecallAgg.value()); + } + } + + @Override + public Optional getResult() { + return Optional.ofNullable(result); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return true; + } + + @Override + public int hashCode() { + return Objects.hashCode(NAME.getPreferredName()); + } + + public static class Result implements EvaluationMetricResult { + + private static final ParseField CLASSES = new ParseField("classes"); + private static final ParseField AVG_RECALL = new ParseField("avg_recall"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("recall_result", true, a -> new Result((List) a[0], (double) a[1])); + + static { + PARSER.declareObjectArray(constructorArg(), PerClassResult.PARSER, CLASSES); + PARSER.declareDouble(constructorArg(), AVG_RECALL); + } + + public static Result fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + /** List of per-class results. */ + private final List classes; + /** Average of per-class recalls. */ + private final double avgRecall; + + public Result(List classes, double avgRecall) { + this.classes = Collections.unmodifiableList(ExceptionsHelper.requireNonNull(classes, CLASSES)); + this.avgRecall = avgRecall; + } + + public Result(StreamInput in) throws IOException { + this.classes = Collections.unmodifiableList(in.readList(PerClassResult::new)); + this.avgRecall = in.readDouble(); + } + + @Override + public String getWriteableName() { + return registeredMetricName(Classification.NAME, NAME); + } + + @Override + public String getMetricName() { + return NAME.getPreferredName(); + } + + public List getClasses() { + return classes; + } + + public double getAvgRecall() { + return avgRecall; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeList(classes); + out.writeDouble(avgRecall); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(CLASSES.getPreferredName(), classes); + builder.field(AVG_RECALL.getPreferredName(), avgRecall); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Result that = (Result) o; + return Objects.equals(this.classes, that.classes) + && this.avgRecall == that.avgRecall; + } + + @Override + public int hashCode() { + return Objects.hash(classes, avgRecall); + } + } + + public static class PerClassResult implements ToXContentObject, Writeable { + + private static final ParseField CLASS_NAME = new ParseField("class_name"); + private static final ParseField RECALL = new ParseField("recall"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("recall_per_class_result", true, a -> new PerClassResult((String) a[0], (double) a[1])); + + static { + PARSER.declareString(constructorArg(), CLASS_NAME); + PARSER.declareDouble(constructorArg(), RECALL); + } + + /** Name of the class. */ + private final String className; + /** Fraction of documents actually belonging to the {@code actualClass} class predicted correctly. */ + private final double recall; + + public PerClassResult(String className, double recall) { + this.className = ExceptionsHelper.requireNonNull(className, CLASS_NAME); + this.recall = recall; + } + + public PerClassResult(StreamInput in) throws IOException { + this.className = in.readString(); + this.recall = in.readDouble(); + } + + public String getClassName() { + return className; + } + + public double getRecall() { + return recall; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(className); + out.writeDouble(recall); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(CLASS_NAME.getPreferredName(), className); + builder.field(RECALL.getPreferredName(), recall); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PerClassResult that = (PerClassResult) o; + return Objects.equals(this.className, that.className) + && this.recall == that.recall; + } + + @Override + public int hashCode() { + return Objects.hash(className, recall); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredError.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredError.java index 0d652f511807e..15a0522d72432 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredError.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredError.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.regression; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ObjectParser; @@ -15,7 +16,9 @@ import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; import java.io.IOException; @@ -25,12 +28,14 @@ import java.util.Objects; import java.util.Optional; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + /** * Calculates the mean squared error between two known numerical fields. * * equation: mse = 1/n * Σ(y - y´)^2 */ -public class MeanSquaredError implements RegressionMetric { +public class MeanSquaredError implements EvaluationMetric { public static final ParseField NAME = new ParseField("mean_squared_error"); @@ -60,11 +65,13 @@ public String getName() { } @Override - public List aggs(String actualField, String predictedField) { + public Tuple, List> aggs(String actualField, String predictedField) { if (result != null) { - return List.of(); + return Tuple.tuple(List.of(), List.of()); } - return List.of(AggregationBuilders.avg(AGG_NAME).script(new Script(buildScript(actualField, predictedField)))); + return Tuple.tuple( + List.of(AggregationBuilders.avg(AGG_NAME).script(new Script(buildScript(actualField, predictedField)))), + List.of()); } @Override @@ -80,7 +87,7 @@ public Optional getResult() { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(Regression.NAME, NAME); } @Override @@ -123,7 +130,7 @@ public Result(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(Regression.NAME, NAME); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquared.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquared.java index a153e7c148ef4..d3a04c415c047 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquared.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquared.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.regression; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ObjectParser; @@ -15,9 +16,11 @@ import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.ExtendedStats; import org.elasticsearch.search.aggregations.metrics.ExtendedStatsAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; import java.io.IOException; @@ -27,6 +30,8 @@ import java.util.Objects; import java.util.Optional; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + /** * Calculates R-Squared between two known numerical fields. * @@ -35,7 +40,7 @@ * SSres = Σ(y - y´)^2, The residual sum of squares * SStot = Σ(y - y_mean)^2, The total sum of squares */ -public class RSquared implements RegressionMetric { +public class RSquared implements EvaluationMetric { public static final ParseField NAME = new ParseField("r_squared"); @@ -65,13 +70,15 @@ public String getName() { } @Override - public List aggs(String actualField, String predictedField) { + public Tuple, List> aggs(String actualField, String predictedField) { if (result != null) { - return List.of(); + return Tuple.tuple(List.of(), List.of()); } - return List.of( - AggregationBuilders.sum(SS_RES).script(new Script(buildScript(actualField, predictedField))), - AggregationBuilders.extendedStats(ExtendedStatsAggregationBuilder.NAME + "_actual").field(actualField)); + return Tuple.tuple( + List.of( + AggregationBuilders.sum(SS_RES).script(new Script(buildScript(actualField, predictedField))), + AggregationBuilders.extendedStats(ExtendedStatsAggregationBuilder.NAME + "_actual").field(actualField)), + List.of()); } @Override @@ -95,7 +102,7 @@ public Optional getResult() { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(Regression.NAME, NAME); } @Override @@ -138,7 +145,7 @@ public Result(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(Regression.NAME, NAME); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/Regression.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/Regression.java index ccf16a9618ec6..cc32ea4049282 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/Regression.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/Regression.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.Evaluation; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.io.IOException; @@ -20,6 +21,8 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + /** * Evaluation of regression results. */ @@ -33,13 +36,13 @@ public class Regression implements Evaluation { @SuppressWarnings("unchecked") public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - NAME.getPreferredName(), a -> new Regression((String) a[0], (String) a[1], (List) a[2])); + NAME.getPreferredName(), a -> new Regression((String) a[0], (String) a[1], (List) a[2])); static { PARSER.declareString(ConstructingObjectParser.constructorArg(), ACTUAL_FIELD); PARSER.declareString(ConstructingObjectParser.constructorArg(), PREDICTED_FIELD); PARSER.declareNamedObjects(ConstructingObjectParser.optionalConstructorArg(), - (p, c, n) -> p.namedObject(RegressionMetric.class, n, c), METRICS); + (p, c, n) -> p.namedObject(EvaluationMetric.class, registeredMetricName(NAME.getPreferredName(), n), c), METRICS); } public static Regression fromXContent(XContentParser parser) { @@ -61,22 +64,22 @@ public static Regression fromXContent(XContentParser parser) { /** * The list of metrics to calculate */ - private final List metrics; + private final List metrics; - public Regression(String actualField, String predictedField, @Nullable List metrics) { + public Regression(String actualField, String predictedField, @Nullable List metrics) { this.actualField = ExceptionsHelper.requireNonNull(actualField, ACTUAL_FIELD); this.predictedField = ExceptionsHelper.requireNonNull(predictedField, PREDICTED_FIELD); this.metrics = initMetrics(metrics, Regression::defaultMetrics); } - private static List defaultMetrics() { + private static List defaultMetrics() { return Arrays.asList(new MeanSquaredError(), new RSquared()); } public Regression(StreamInput in) throws IOException { this.actualField = in.readString(); this.predictedField = in.readString(); - this.metrics = in.readNamedWriteableList(RegressionMetric.class); + this.metrics = in.readNamedWriteableList(EvaluationMetric.class); } @Override @@ -95,7 +98,7 @@ public String getPredictedField() { } @Override - public List getMetrics() { + public List getMetrics() { return metrics; } @@ -118,8 +121,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(PREDICTED_FIELD.getPreferredName(), predictedField); builder.startObject(METRICS.getPreferredName()); - for (RegressionMetric metric : metrics) { - builder.field(metric.getWriteableName(), metric); + for (EvaluationMetric metric : metrics) { + builder.field(metric.getName(), metric); } builder.endObject(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RegressionMetric.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RegressionMetric.java deleted file mode 100644 index 5b46829b4c852..0000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RegressionMetric.java +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.core.ml.dataframe.evaluation.regression; - -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; - -public interface RegressionMetric extends EvaluationMetric { -} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/AbstractConfusionMatrixMetric.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/AbstractConfusionMatrixMetric.java index 286a68314d713..e09e03b1208aa 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/AbstractConfusionMatrixMetric.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/AbstractConfusionMatrixMetric.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -15,6 +16,8 @@ import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; @@ -22,9 +25,9 @@ import java.util.List; import java.util.Optional; -import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.SoftClassificationMetric.actualIsTrueQuery; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.BinarySoftClassification.actualIsTrueQuery; -abstract class AbstractConfusionMatrixMetric implements SoftClassificationMetric { +abstract class AbstractConfusionMatrixMetric implements EvaluationMetric { public static final ParseField AT = new ParseField("at"); @@ -62,11 +65,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } @Override - public final List aggs(String actualField, String predictedProbabilityField) { + public Tuple, List> aggs(String actualField, String predictedProbabilityField) { if (result != null) { - return List.of(); + return Tuple.tuple(List.of(), List.of()); } - return aggsAt(actualField, predictedProbabilityField); + return Tuple.tuple(aggsAt(actualField, predictedProbabilityField), List.of()); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/AucRoc.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/AucRoc.java index 40257ebce4cdb..3f62a0b144eec 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/AucRoc.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/AucRoc.java @@ -7,6 +7,7 @@ import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -18,8 +19,10 @@ import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.search.aggregations.metrics.Percentiles; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; @@ -33,7 +36,8 @@ import java.util.Optional; import java.util.stream.IntStream; -import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.SoftClassificationMetric.actualIsTrueQuery; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification.BinarySoftClassification.actualIsTrueQuery; /** * Area under the curve (AUC) of the receiver operating characteristic (ROC). @@ -53,7 +57,7 @@ * When this is used for multi-class classification, it will calculate the ROC * curve of each class versus the rest. */ -public class AucRoc implements SoftClassificationMetric { +public class AucRoc implements EvaluationMetric { public static final ParseField NAME = new ParseField("auc_roc"); @@ -88,7 +92,7 @@ public AucRoc(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(BinarySoftClassification.NAME, NAME); } @Override @@ -123,9 +127,9 @@ public int hashCode() { } @Override - public List aggs(String actualField, String predictedProbabilityField) { + public Tuple, List> aggs(String actualField, String predictedProbabilityField) { if (result != null) { - return List.of(); + return Tuple.tuple(List.of(), List.of()); } double[] percentiles = IntStream.range(1, 100).mapToDouble(v -> (double) v).toArray(); AggregationBuilder percentilesForClassValueAgg = @@ -138,7 +142,9 @@ public List aggs(String actualField, String predictedProbabi .filter(NON_TRUE_AGG_NAME, QueryBuilders.boolQuery().mustNot(actualIsTrueQuery(actualField))) .subAggregation( AggregationBuilders.percentiles(PERCENTILES).field(predictedProbabilityField).percentiles(percentiles)); - return List.of(percentilesForClassValueAgg, percentilesForRestAgg); + return Tuple.tuple( + List.of(percentilesForClassValueAgg, percentilesForRestAgg), + List.of()); } @Override @@ -330,7 +336,7 @@ public Result(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(BinarySoftClassification.NAME, NAME); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/BinarySoftClassification.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/BinarySoftClassification.java index 67a635e078be2..8d4f4f01d02cd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/BinarySoftClassification.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/BinarySoftClassification.java @@ -12,7 +12,10 @@ import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.Evaluation; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.io.IOException; @@ -20,6 +23,8 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + /** * Evaluation of binary soft classification methods, e.g. outlier detection. * This is useful to evaluate problems where a model outputs a probability of whether @@ -34,19 +39,23 @@ public class BinarySoftClassification implements Evaluation { private static final ParseField METRICS = new ParseField("metrics"); public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - NAME.getPreferredName(), a -> new BinarySoftClassification((String) a[0], (String) a[1], (List) a[2])); + NAME.getPreferredName(), a -> new BinarySoftClassification((String) a[0], (String) a[1], (List) a[2])); static { PARSER.declareString(ConstructingObjectParser.constructorArg(), ACTUAL_FIELD); PARSER.declareString(ConstructingObjectParser.constructorArg(), PREDICTED_PROBABILITY_FIELD); PARSER.declareNamedObjects(ConstructingObjectParser.optionalConstructorArg(), - (p, c, n) -> p.namedObject(SoftClassificationMetric.class, n, null), METRICS); + (p, c, n) -> p.namedObject(EvaluationMetric.class, registeredMetricName(NAME.getPreferredName(), n), c), METRICS); } public static BinarySoftClassification fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } + static QueryBuilder actualIsTrueQuery(String actualField) { + return QueryBuilders.queryStringQuery(actualField + ": (1 OR true)"); + } + /** * The field where the actual class is marked up. * The value of this field is assumed to either be 1 or 0, or true or false. @@ -61,16 +70,16 @@ public static BinarySoftClassification fromXContent(XContentParser parser) { /** * The list of metrics to calculate */ - private final List metrics; + private final List metrics; public BinarySoftClassification(String actualField, String predictedProbabilityField, - @Nullable List metrics) { + @Nullable List metrics) { this.actualField = ExceptionsHelper.requireNonNull(actualField, ACTUAL_FIELD); this.predictedProbabilityField = ExceptionsHelper.requireNonNull(predictedProbabilityField, PREDICTED_PROBABILITY_FIELD); this.metrics = initMetrics(metrics, BinarySoftClassification::defaultMetrics); } - private static List defaultMetrics() { + private static List defaultMetrics() { return Arrays.asList( new AucRoc(false), new Precision(Arrays.asList(0.25, 0.5, 0.75)), @@ -81,7 +90,7 @@ private static List defaultMetrics() { public BinarySoftClassification(StreamInput in) throws IOException { this.actualField = in.readString(); this.predictedProbabilityField = in.readString(); - this.metrics = in.readNamedWriteableList(SoftClassificationMetric.class); + this.metrics = in.readNamedWriteableList(EvaluationMetric.class); } @Override @@ -100,7 +109,7 @@ public String getPredictedField() { } @Override - public List getMetrics() { + public List getMetrics() { return metrics; } @@ -123,7 +132,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(PREDICTED_PROBABILITY_FIELD.getPreferredName(), predictedProbabilityField); builder.startObject(METRICS.getPreferredName()); - for (SoftClassificationMetric metric : metrics) { + for (EvaluationMetric metric : metrics) { builder.field(metric.getName(), metric); } builder.endObject(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ConfusionMatrix.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ConfusionMatrix.java index d52468a0214b6..1b1d5b8f9d170 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ConfusionMatrix.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ConfusionMatrix.java @@ -21,6 +21,8 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + public class ConfusionMatrix extends AbstractConfusionMatrixMetric { public static final ParseField NAME = new ParseField("confusion_matrix"); @@ -46,7 +48,7 @@ public ConfusionMatrix(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(BinarySoftClassification.NAME, NAME); } @Override @@ -129,7 +131,7 @@ public Result(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(BinarySoftClassification.NAME, NAME); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/Precision.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/Precision.java index 80f838dd5d166..d05ddb5fc4c9b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/Precision.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/Precision.java @@ -19,6 +19,8 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + public class Precision extends AbstractConfusionMatrixMetric { public static final ParseField NAME = new ParseField("precision"); @@ -44,7 +46,7 @@ public Precision(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(BinarySoftClassification.NAME, NAME); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/Recall.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/Recall.java index 70bda8099db89..2dd44aff6715d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/Recall.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/Recall.java @@ -19,6 +19,8 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + public class Recall extends AbstractConfusionMatrixMetric { public static final ParseField NAME = new ParseField("recall"); @@ -44,7 +46,7 @@ public Recall(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME.getPreferredName(); + return registeredMetricName(BinarySoftClassification.NAME, NAME); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ScoreByThresholdResult.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ScoreByThresholdResult.java index 0ad99a83cf25b..8fdb06bde4d6e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ScoreByThresholdResult.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/ScoreByThresholdResult.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -13,9 +14,11 @@ import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; + public class ScoreByThresholdResult implements EvaluationMetricResult { - public static final String NAME = "score_by_threshold_result"; + public static final ParseField NAME = new ParseField("score_by_threshold_result"); private final String name; private final double[] thresholds; @@ -36,7 +39,7 @@ public ScoreByThresholdResult(StreamInput in) throws IOException { @Override public String getWriteableName() { - return NAME; + return registeredMetricName(BinarySoftClassification.NAME, NAME); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/SoftClassificationMetric.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/SoftClassificationMetric.java deleted file mode 100644 index 9a9c382caf9d1..0000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/SoftClassificationMetric.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.core.ml.dataframe.evaluation.softclassification; - -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; - -public interface SoftClassificationMetric extends EvaluationMetric { - - static QueryBuilder actualIsTrueQuery(String actualField) { - return QueryBuilders.queryStringQuery(actualField + ": (1 OR true)"); - } -} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/EvaluateDataFrameActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/EvaluateDataFrameActionRequestTests.java index 51fc319642d59..dd485341cb7fd 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/EvaluateDataFrameActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/EvaluateDataFrameActionRequestTests.java @@ -31,7 +31,7 @@ public class EvaluateDataFrameActionRequestTests extends AbstractSerializingTest @Override protected NamedWriteableRegistry getNamedWriteableRegistry() { List namedWriteables = new ArrayList<>(); - namedWriteables.addAll(new MlEvaluationNamedXContentProvider().getNamedWriteables()); + namedWriteables.addAll(MlEvaluationNamedXContentProvider.getNamedWriteables()); namedWriteables.addAll(new SearchModule(Settings.EMPTY, Collections.emptyList()).getNamedWriteables()); return new NamedWriteableRegistry(namedWriteables); } @@ -46,13 +46,11 @@ protected NamedXContentRegistry xContentRegistry() { @Override protected Request createTestInstance() { - Request request = new Request(); int indicesCount = randomIntBetween(1, 5); List indices = new ArrayList<>(indicesCount); for (int i = 0; i < indicesCount; i++) { indices.add(randomAlphaOfLength(10)); } - request.setIndices(indices); QueryProvider queryProvider = null; if (randomBoolean()) { try { @@ -62,10 +60,11 @@ protected Request createTestInstance() { throw new UncheckedIOException(e); } } - request.setQueryProvider(queryProvider); Evaluation evaluation = randomBoolean() ? BinarySoftClassificationTests.createRandom() : RegressionTests.createRandom(); - request.setEvaluation(evaluation); - return request; + return new Request() + .setIndices(indices) + .setQueryProvider(queryProvider) + .setEvaluation(evaluation); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/EvaluateDataFrameActionResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/EvaluateDataFrameActionResponseTests.java index 8aedcb2bfa641..437734ac763be 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/EvaluateDataFrameActionResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/EvaluateDataFrameActionResponseTests.java @@ -11,7 +11,10 @@ import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction.Response; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.AccuracyResultTests; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrixResultTests; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.PrecisionResultTests; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.RecallResultTests; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.regression.MeanSquaredError; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.regression.RSquared; @@ -21,7 +24,7 @@ public class EvaluateDataFrameActionResponseTests extends AbstractWireSerializin @Override protected NamedWriteableRegistry getNamedWriteableRegistry() { - return new NamedWriteableRegistry(new MlEvaluationNamedXContentProvider().getNamedWriteables()); + return new NamedWriteableRegistry(MlEvaluationNamedXContentProvider.getNamedWriteables()); } @Override @@ -29,11 +32,13 @@ protected Response createTestInstance() { String evaluationName = randomAlphaOfLength(10); List metrics = List.of( + AccuracyResultTests.createRandom(), + PrecisionResultTests.createRandom(), + RecallResultTests.createRandom(), MulticlassConfusionMatrixResultTests.createRandom(), new MeanSquaredError.Result(randomDouble()), new RSquared.Result(randomDouble())); - int numMetrics = randomIntBetween(0, metrics.size()); - return new Response(evaluationName, metrics.subList(0, numMetrics)); + return new Response(evaluationName, randomSubsetOf(metrics)); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyResultTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyResultTests.java index bb3cc99192067..8fb4c6c02408d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyResultTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyResultTests.java @@ -17,15 +17,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -public class AccuracyResultTests extends AbstractWireSerializingTestCase { +public class AccuracyResultTests extends AbstractWireSerializingTestCase { - @Override - protected NamedWriteableRegistry getNamedWriteableRegistry() { - return new NamedWriteableRegistry(new MlEvaluationNamedXContentProvider().getNamedWriteables()); - } - - @Override - protected Accuracy.Result createTestInstance() { + public static Result createRandom() { int numClasses = randomIntBetween(2, 100); List classNames = Stream.generate(() -> randomAlphaOfLength(10)).limit(numClasses).collect(Collectors.toList()); List actualClasses = new ArrayList<>(numClasses); @@ -38,7 +32,17 @@ protected Accuracy.Result createTestInstance() { } @Override - protected Writeable.Reader instanceReader() { - return Accuracy.Result::new; + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(MlEvaluationNamedXContentProvider.getNamedWriteables()); + } + + @Override + protected Result createTestInstance() { + return createRandom(); + } + + @Override + protected Writeable.Reader instanceReader() { + return Result::new; } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java index 23c2effb37fe9..f205974809961 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java @@ -8,6 +8,7 @@ import org.apache.lucene.search.TotalHits; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -19,8 +20,10 @@ import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; @@ -42,7 +45,7 @@ public class ClassificationTests extends AbstractSerializingTestCase metrics = + List metrics = randomSubsetOf( Arrays.asList( AccuracyTests.createRandom(), + PrecisionTests.createRandom(), + RecallTests.createRandom(), MulticlassConfusionMatrixTests.createRandom())); return new Classification(randomAlphaOfLength(10), randomAlphaOfLength(10), metrics.isEmpty() ? null : metrics); } @@ -101,10 +106,10 @@ public void testBuildSearch() { } public void testProcess_MultipleMetricsWithDifferentNumberOfSteps() { - ClassificationMetric metric1 = new FakeClassificationMetric("fake_metric_1", 2); - ClassificationMetric metric2 = new FakeClassificationMetric("fake_metric_2", 3); - ClassificationMetric metric3 = new FakeClassificationMetric("fake_metric_3", 4); - ClassificationMetric metric4 = new FakeClassificationMetric("fake_metric_4", 5); + EvaluationMetric metric1 = new FakeClassificationMetric("fake_metric_1", 2); + EvaluationMetric metric2 = new FakeClassificationMetric("fake_metric_2", 3); + EvaluationMetric metric3 = new FakeClassificationMetric("fake_metric_3", 4); + EvaluationMetric metric4 = new FakeClassificationMetric("fake_metric_4", 5); Classification evaluation = new Classification("act", "pred", Arrays.asList(metric1, metric2, metric3, metric4)); assertThat(metric1.getResult(), isEmpty()); @@ -168,7 +173,7 @@ private static SearchResponse mockSearchResponseWithNonZeroTotalHits() { * Number of steps is configurable. * Upon reaching the last step, the result is produced. */ - private static class FakeClassificationMetric implements ClassificationMetric { + private static class FakeClassificationMetric implements EvaluationMetric { private final String name; private final int numSteps; @@ -191,8 +196,8 @@ public String getWriteableName() { } @Override - public List aggs(String actualField, String predictedField) { - return List.of(); + public Tuple, List> aggs(String actualField, String predictedField) { + return Tuple.tuple(List.of(), List.of()); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java index a04e47ff26e65..f145a06c3c894 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java @@ -6,10 +6,12 @@ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.ActualClass; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.PredictedClass; @@ -23,9 +25,9 @@ import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockFiltersBucket; import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockTerms; import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockTermsBucket; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.TupleMatchers.isTuple; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; public class MulticlassConfusionMatrixTests extends AbstractSerializingTestCase { @@ -72,8 +74,8 @@ public void testConstructor_SizeValidationFailures() { public void testAggs() { MulticlassConfusionMatrix confusionMatrix = new MulticlassConfusionMatrix(); - List aggs = confusionMatrix.aggs("act", "pred"); - assertThat(aggs, is(not(empty()))); + Tuple, List> aggs = confusionMatrix.aggs("act", "pred"); + assertThat(aggs, isTuple(not(empty()), empty())); assertThat(confusionMatrix.getResult(), isEmpty()); } @@ -105,7 +107,7 @@ public void testEvaluate() { MulticlassConfusionMatrix confusionMatrix = new MulticlassConfusionMatrix(2); confusionMatrix.process(aggs); - assertThat(confusionMatrix.aggs("act", "pred"), is(empty())); + assertThat(confusionMatrix.aggs("act", "pred"), isTuple(empty(), empty())); MulticlassConfusionMatrix.Result result = (MulticlassConfusionMatrix.Result) confusionMatrix.getResult().get(); assertThat(result.getMetricName(), equalTo("multiclass_confusion_matrix")); assertThat( @@ -145,7 +147,7 @@ public void testEvaluate_OtherClassesCountGreaterThanZero() { MulticlassConfusionMatrix confusionMatrix = new MulticlassConfusionMatrix(2); confusionMatrix.process(aggs); - assertThat(confusionMatrix.aggs("act", "pred"), is(empty())); + assertThat(confusionMatrix.aggs("act", "pred"), isTuple(empty(), empty())); MulticlassConfusionMatrix.Result result = (MulticlassConfusionMatrix.Result) confusionMatrix.getResult().get(); assertThat(result.getMetricName(), equalTo("multiclass_confusion_matrix")); assertThat( diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionResultTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionResultTests.java new file mode 100644 index 0000000000000..b86448a4daacb --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionResultTests.java @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision.PerClassResult; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision.Result; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class PrecisionResultTests extends AbstractWireSerializingTestCase { + + public static Result createRandom() { + int numClasses = randomIntBetween(2, 100); + List classNames = Stream.generate(() -> randomAlphaOfLength(10)).limit(numClasses).collect(Collectors.toList()); + List classes = new ArrayList<>(numClasses); + for (int i = 0; i < numClasses; i++) { + double precision = randomDoubleBetween(0.0, 1.0, true); + classes.add(new PerClassResult(classNames.get(i), precision)); + } + double avgPrecision = randomDoubleBetween(0.0, 1.0, true); + return new Result(classes, avgPrecision); + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(MlEvaluationNamedXContentProvider.getNamedWriteables()); + } + + @Override + protected Result createTestInstance() { + return createRandom(); + } + + @Override + protected Writeable.Reader instanceReader() { + return Result::new; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionTests.java new file mode 100644 index 0000000000000..3000edf5b68b5 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionTests.java @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; + +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.test.hamcrest.OptionalMatchers.isEmpty; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockFilters; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockSingleValue; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockTerms; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.TupleMatchers.isTuple; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; + +public class PrecisionTests extends AbstractSerializingTestCase { + + @Override + protected Precision doParseInstance(XContentParser parser) throws IOException { + return Precision.fromXContent(parser); + } + + @Override + protected Precision createTestInstance() { + return createRandom(); + } + + @Override + protected Writeable.Reader instanceReader() { + return Precision::new; + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + public static Precision createRandom() { + return new Precision(); + } + + public void testProcess() { + Aggregations aggs = new Aggregations(Arrays.asList( + mockTerms(Precision.ACTUAL_CLASSES_NAMES_AGG_NAME), + mockFilters(Precision.BY_PREDICTED_CLASS_AGG_NAME), + mockSingleValue(Precision.AVG_PRECISION_AGG_NAME, 0.8123), + mockSingleValue("some_other_single_metric_agg", 0.2377) + )); + + Precision precision = new Precision(); + precision.process(aggs); + + assertThat(precision.aggs("act", "pred"), isTuple(empty(), empty())); + assertThat(precision.getResult().get(), equalTo(new Precision.Result(List.of(), 0.8123))); + } + + public void testProcess_GivenMissingAgg() { + { + Aggregations aggs = new Aggregations(Arrays.asList( + mockFilters(Precision.BY_PREDICTED_CLASS_AGG_NAME), + mockSingleValue("some_other_single_metric_agg", 0.2377) + )); + Precision precision = new Precision(); + precision.process(aggs); + assertThat(precision.getResult(), isEmpty()); + } + { + Aggregations aggs = new Aggregations(Arrays.asList( + mockSingleValue(Precision.AVG_PRECISION_AGG_NAME, 0.8123), + mockSingleValue("some_other_single_metric_agg", 0.2377) + )); + Precision precision = new Precision(); + precision.process(aggs); + assertThat(precision.getResult(), isEmpty()); + } + } + + public void testProcess_GivenAggOfWrongType() { + { + Aggregations aggs = new Aggregations(Arrays.asList( + mockFilters(Precision.BY_PREDICTED_CLASS_AGG_NAME), + mockFilters(Precision.AVG_PRECISION_AGG_NAME) + )); + Precision precision = new Precision(); + precision.process(aggs); + assertThat(precision.getResult(), isEmpty()); + } + { + Aggregations aggs = new Aggregations(Arrays.asList( + mockSingleValue(Precision.BY_PREDICTED_CLASS_AGG_NAME, 1.0), + mockSingleValue(Precision.AVG_PRECISION_AGG_NAME, 0.8123) + )); + Precision precision = new Precision(); + precision.process(aggs); + assertThat(precision.getResult(), isEmpty()); + } + } + + public void testProcess_GivenCardinalityTooHigh() { + Aggregations aggs = + new Aggregations(Collections.singletonList(mockTerms(Precision.ACTUAL_CLASSES_NAMES_AGG_NAME, Collections.emptyList(), 1))); + Precision precision = new Precision(); + precision.aggs("foo", "bar"); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> precision.process(aggs)); + assertThat(e.getMessage(), containsString("Cardinality of field [foo] is too high")); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallResultTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallResultTests.java new file mode 100644 index 0000000000000..a2a44ded76189 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallResultTests.java @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall.PerClassResult; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall.Result; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class RecallResultTests extends AbstractWireSerializingTestCase { + + public static Result createRandom() { + int numClasses = randomIntBetween(2, 100); + List classNames = Stream.generate(() -> randomAlphaOfLength(10)).limit(numClasses).collect(Collectors.toList()); + List classes = new ArrayList<>(numClasses); + for (int i = 0; i < numClasses; i++) { + double recall = randomDoubleBetween(0.0, 1.0, true); + classes.add(new PerClassResult(classNames.get(i), recall)); + } + double avgRecall = randomDoubleBetween(0.0, 1.0, true); + return new Result(classes, avgRecall); + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(MlEvaluationNamedXContentProvider.getNamedWriteables()); + } + + @Override + protected Result createTestInstance() { + return createRandom(); + } + + @Override + protected Writeable.Reader instanceReader() { + return Result::new; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallTests.java new file mode 100644 index 0000000000000..877f79e336fec --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallTests.java @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; + +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.test.hamcrest.OptionalMatchers.isEmpty; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockSingleValue; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockTerms; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.TupleMatchers.isTuple; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; + +public class RecallTests extends AbstractSerializingTestCase { + + @Override + protected Recall doParseInstance(XContentParser parser) throws IOException { + return Recall.fromXContent(parser); + } + + @Override + protected Recall createTestInstance() { + return createRandom(); + } + + @Override + protected Writeable.Reader instanceReader() { + return Recall::new; + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + public static Recall createRandom() { + return new Recall(); + } + + public void testProcess() { + Aggregations aggs = new Aggregations(Arrays.asList( + mockTerms(Recall.BY_ACTUAL_CLASS_AGG_NAME), + mockSingleValue(Recall.AVG_RECALL_AGG_NAME, 0.8123), + mockSingleValue("some_other_single_metric_agg", 0.2377) + )); + + Recall recall = new Recall(); + recall.process(aggs); + + assertThat(recall.aggs("act", "pred"), isTuple(empty(), empty())); + assertThat(recall.getResult().get(), equalTo(new Recall.Result(List.of(), 0.8123))); + } + + public void testProcess_GivenMissingAgg() { + { + Aggregations aggs = new Aggregations(Arrays.asList( + mockTerms(Recall.BY_ACTUAL_CLASS_AGG_NAME), + mockSingleValue("some_other_single_metric_agg", 0.2377) + )); + Recall recall = new Recall(); + recall.process(aggs); + assertThat(recall.getResult(), isEmpty()); + } + { + Aggregations aggs = new Aggregations(Arrays.asList( + mockSingleValue(Recall.AVG_RECALL_AGG_NAME, 0.8123), + mockSingleValue("some_other_single_metric_agg", 0.2377) + )); + Recall recall = new Recall(); + recall.process(aggs); + assertThat(recall.getResult(), isEmpty()); + } + } + + public void testProcess_GivenAggOfWrongType() { + { + Aggregations aggs = new Aggregations(Arrays.asList( + mockTerms(Recall.BY_ACTUAL_CLASS_AGG_NAME), + mockTerms(Recall.AVG_RECALL_AGG_NAME) + )); + Recall recall = new Recall(); + recall.process(aggs); + assertThat(recall.getResult(), isEmpty()); + } + { + Aggregations aggs = new Aggregations(Arrays.asList( + mockSingleValue(Recall.BY_ACTUAL_CLASS_AGG_NAME, 1.0), + mockSingleValue(Recall.AVG_RECALL_AGG_NAME, 0.8123) + )); + Recall recall = new Recall(); + recall.process(aggs); + assertThat(recall.getResult(), isEmpty()); + } + } + + public void testProcess_GivenCardinalityTooHigh() { + Aggregations aggs = new Aggregations(Arrays.asList( + mockTerms(Recall.BY_ACTUAL_CLASS_AGG_NAME, Collections.emptyList(), 1), + mockSingleValue(Recall.AVG_RECALL_AGG_NAME, 0.8123))); + Recall recall = new Recall(); + recall.aggs("foo", "bar"); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> recall.process(aggs)); + assertThat(e.getMessage(), containsString("Cardinality of field [foo] is too high")); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/TupleMatchers.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/TupleMatchers.java new file mode 100644 index 0000000000000..8bd8ff7572f54 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/TupleMatchers.java @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; + +import org.elasticsearch.common.collect.Tuple; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import java.util.Arrays; + +public class TupleMatchers { + + private static class TupleMatcher extends TypeSafeMatcher> { + + private final Matcher v1Matcher; + private final Matcher v2Matcher; + + private TupleMatcher(Matcher v1Matcher, Matcher v2Matcher) { + this.v1Matcher = v1Matcher; + this.v2Matcher = v2Matcher; + } + + @Override + protected boolean matchesSafely(final Tuple item) { + return item != null && v1Matcher.matches(item.v1()) && v2Matcher.matches(item.v2()); + } + + @Override + public void describeTo(final Description description) { + description.appendText("expected tuple matching ").appendList("[", ", ", "]", Arrays.asList(v1Matcher, v2Matcher)); + } + } + + /** + * Creates a matcher that matches iff: + * 1. the examined tuple's v1() matches the specified v1Matcher + * and + * 2. the examined tuple's v2() matches the specified v2Matcher + * For example: + *

    assertThat(Tuple.tuple("myValue1", "myValue2"), isTuple(startsWith("my"), containsString("Val")))
    + */ + public static TupleMatcher isTuple(Matcher v1Matcher, Matcher v2Matcher) { + return new TupleMatcher(v1Matcher, v2Matcher); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RegressionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RegressionTests.java index 077998b66aed0..96ba97ecc9348 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RegressionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RegressionTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; import java.io.IOException; @@ -29,7 +30,7 @@ public class RegressionTests extends AbstractSerializingTestCase { @Override protected NamedWriteableRegistry getNamedWriteableRegistry() { - return new NamedWriteableRegistry(new MlEvaluationNamedXContentProvider().getNamedWriteables()); + return new NamedWriteableRegistry(MlEvaluationNamedXContentProvider.getNamedWriteables()); } @Override @@ -38,7 +39,7 @@ protected NamedXContentRegistry xContentRegistry() { } public static Regression createRandom() { - List metrics = new ArrayList<>(); + List metrics = new ArrayList<>(); if (randomBoolean()) { metrics.add(MeanSquaredErrorTests.createRandom()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/BinarySoftClassificationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/BinarySoftClassificationTests.java index e63e88f6f848f..28e0a045b190d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/BinarySoftClassificationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/softclassification/BinarySoftClassificationTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; import java.io.IOException; @@ -29,7 +30,7 @@ public class BinarySoftClassificationTests extends AbstractSerializingTestCase metrics = new ArrayList<>(); + List metrics = new ArrayList<>(); if (randomBoolean()) { metrics.add(AucRocTests.createRandom()); } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java index 6fd7e289fc231..14c2c3c9aca61 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java @@ -5,21 +5,23 @@ */ package org.elasticsearch.xpack.ml.integration; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Classification; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.ActualClass; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.PredictedClass; import org.junit.After; import org.junit.Before; import java.util.List; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -116,6 +118,68 @@ public void testEvaluate_Accuracy_BooleanField() { assertThat(accuracyResult.getOverallAccuracy(), equalTo(45.0 / 75)); } + public void testEvaluate_Precision() { + EvaluateDataFrameAction.Response evaluateDataFrameResponse = + evaluateDataFrame( + ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new Precision()))); + + assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); + assertThat(evaluateDataFrameResponse.getMetrics(), hasSize(1)); + + Precision.Result precisionResult = (Precision.Result) evaluateDataFrameResponse.getMetrics().get(0); + assertThat(precisionResult.getMetricName(), equalTo(Precision.NAME.getPreferredName())); + assertThat( + precisionResult.getClasses(), + equalTo( + List.of( + new Precision.PerClassResult("ant", 1.0 / 15), + new Precision.PerClassResult("cat", 1.0 / 15), + new Precision.PerClassResult("dog", 1.0 / 15), + new Precision.PerClassResult("fox", 1.0 / 15), + new Precision.PerClassResult("mouse", 1.0 / 15)))); + assertThat(precisionResult.getAvgPrecision(), equalTo(5.0 / 75)); + } + + public void testEvaluate_Precision_CardinalityTooHigh() { + ElasticsearchStatusException e = + expectThrows( + ElasticsearchStatusException.class, + () -> evaluateDataFrame( + ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new Precision(4))))); + assertThat(e.getMessage(), containsString("Cardinality of field [animal_name] is too high")); + } + + public void testEvaluate_Recall() { + EvaluateDataFrameAction.Response evaluateDataFrameResponse = + evaluateDataFrame( + ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new Recall()))); + + assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); + assertThat(evaluateDataFrameResponse.getMetrics(), hasSize(1)); + + Recall.Result recallResult = (Recall.Result) evaluateDataFrameResponse.getMetrics().get(0); + assertThat(recallResult.getMetricName(), equalTo(Recall.NAME.getPreferredName())); + assertThat( + recallResult.getClasses(), + equalTo( + List.of( + new Recall.PerClassResult("ant", 1.0 / 15), + new Recall.PerClassResult("cat", 1.0 / 15), + new Recall.PerClassResult("dog", 1.0 / 15), + new Recall.PerClassResult("fox", 1.0 / 15), + new Recall.PerClassResult("mouse", 1.0 / 15)))); + assertThat(recallResult.getAvgRecall(), equalTo(5.0 / 75)); + } + + public void testEvaluate_Recall_CardinalityTooHigh() { + ElasticsearchStatusException e = + expectThrows( + ElasticsearchStatusException.class, + () -> evaluateDataFrame( + ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new Recall(4))))); + assertThat(e.getMessage(), containsString("Cardinality of field [animal_name] is too high")); + } + public void testEvaluate_ConfusionMatrixMetricWithDefaultSize() { EvaluateDataFrameAction.Response evaluateDataFrameResponse = evaluateDataFrame( @@ -131,50 +195,50 @@ public void testEvaluate_ConfusionMatrixMetricWithDefaultSize() { assertThat( confusionMatrixResult.getConfusionMatrix(), equalTo(List.of( - new ActualClass("ant", + new MulticlassConfusionMatrix.ActualClass("ant", 15, List.of( - new PredictedClass("ant", 1L), - new PredictedClass("cat", 4L), - new PredictedClass("dog", 3L), - new PredictedClass("fox", 2L), - new PredictedClass("mouse", 5L)), + new MulticlassConfusionMatrix.PredictedClass("ant", 1L), + new MulticlassConfusionMatrix.PredictedClass("cat", 4L), + new MulticlassConfusionMatrix.PredictedClass("dog", 3L), + new MulticlassConfusionMatrix.PredictedClass("fox", 2L), + new MulticlassConfusionMatrix.PredictedClass("mouse", 5L)), 0), - new ActualClass("cat", + new MulticlassConfusionMatrix.ActualClass("cat", 15, List.of( - new PredictedClass("ant", 3L), - new PredictedClass("cat", 1L), - new PredictedClass("dog", 5L), - new PredictedClass("fox", 4L), - new PredictedClass("mouse", 2L)), + new MulticlassConfusionMatrix.PredictedClass("ant", 3L), + new MulticlassConfusionMatrix.PredictedClass("cat", 1L), + new MulticlassConfusionMatrix.PredictedClass("dog", 5L), + new MulticlassConfusionMatrix.PredictedClass("fox", 4L), + new MulticlassConfusionMatrix.PredictedClass("mouse", 2L)), 0), - new ActualClass("dog", + new MulticlassConfusionMatrix.ActualClass("dog", 15, List.of( - new PredictedClass("ant", 4L), - new PredictedClass("cat", 2L), - new PredictedClass("dog", 1L), - new PredictedClass("fox", 5L), - new PredictedClass("mouse", 3L)), + new MulticlassConfusionMatrix.PredictedClass("ant", 4L), + new MulticlassConfusionMatrix.PredictedClass("cat", 2L), + new MulticlassConfusionMatrix.PredictedClass("dog", 1L), + new MulticlassConfusionMatrix.PredictedClass("fox", 5L), + new MulticlassConfusionMatrix.PredictedClass("mouse", 3L)), 0), - new ActualClass("fox", + new MulticlassConfusionMatrix.ActualClass("fox", 15, List.of( - new PredictedClass("ant", 5L), - new PredictedClass("cat", 3L), - new PredictedClass("dog", 2L), - new PredictedClass("fox", 1L), - new PredictedClass("mouse", 4L)), + new MulticlassConfusionMatrix.PredictedClass("ant", 5L), + new MulticlassConfusionMatrix.PredictedClass("cat", 3L), + new MulticlassConfusionMatrix.PredictedClass("dog", 2L), + new MulticlassConfusionMatrix.PredictedClass("fox", 1L), + new MulticlassConfusionMatrix.PredictedClass("mouse", 4L)), 0), - new ActualClass("mouse", + new MulticlassConfusionMatrix.ActualClass("mouse", 15, List.of( - new PredictedClass("ant", 2L), - new PredictedClass("cat", 5L), - new PredictedClass("dog", 4L), - new PredictedClass("fox", 3L), - new PredictedClass("mouse", 1L)), + new MulticlassConfusionMatrix.PredictedClass("ant", 2L), + new MulticlassConfusionMatrix.PredictedClass("cat", 5L), + new MulticlassConfusionMatrix.PredictedClass("dog", 4L), + new MulticlassConfusionMatrix.PredictedClass("fox", 3L), + new MulticlassConfusionMatrix.PredictedClass("mouse", 1L)), 0)))); assertThat(confusionMatrixResult.getOtherActualClassCount(), equalTo(0L)); } @@ -193,17 +257,26 @@ public void testEvaluate_ConfusionMatrixMetricWithUserProvidedSize() { assertThat( confusionMatrixResult.getConfusionMatrix(), equalTo(List.of( - new ActualClass("ant", + new MulticlassConfusionMatrix.ActualClass("ant", 15, - List.of(new PredictedClass("ant", 1L), new PredictedClass("cat", 4L), new PredictedClass("dog", 3L)), + List.of( + new MulticlassConfusionMatrix.PredictedClass("ant", 1L), + new MulticlassConfusionMatrix.PredictedClass("cat", 4L), + new MulticlassConfusionMatrix.PredictedClass("dog", 3L)), 7), - new ActualClass("cat", + new MulticlassConfusionMatrix.ActualClass("cat", 15, - List.of(new PredictedClass("ant", 3L), new PredictedClass("cat", 1L), new PredictedClass("dog", 5L)), + List.of( + new MulticlassConfusionMatrix.PredictedClass("ant", 3L), + new MulticlassConfusionMatrix.PredictedClass("cat", 1L), + new MulticlassConfusionMatrix.PredictedClass("dog", 5L)), 6), - new ActualClass("dog", + new MulticlassConfusionMatrix.ActualClass("dog", 15, - List.of(new PredictedClass("ant", 4L), new PredictedClass("cat", 2L), new PredictedClass("dog", 1L)), + List.of( + new MulticlassConfusionMatrix.PredictedClass("ant", 4L), + new MulticlassConfusionMatrix.PredictedClass("cat", 2L), + new MulticlassConfusionMatrix.PredictedClass("dog", 1L)), 8)))); assertThat(confusionMatrixResult.getOtherActualClassCount(), equalTo(2L)); } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java index 40138b826e462..87fa5c30b0755 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java @@ -27,6 +27,8 @@ import org.elasticsearch.xpack.core.ml.dataframe.analyses.Classification; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall; import org.junit.After; import java.util.ArrayList; @@ -450,9 +452,11 @@ private void assertEvaluation(String dependentVariable, List dependentVar evaluateDataFrame( destIndex, new org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Classification( - dependentVariable, predictedClassField, Arrays.asList(new Accuracy(), new MulticlassConfusionMatrix()))); + dependentVariable, + predictedClassField, + Arrays.asList(new Accuracy(), new MulticlassConfusionMatrix(), new Precision(), new Recall()))); assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); - assertThat(evaluateDataFrameResponse.getMetrics().size(), equalTo(2)); + assertThat(evaluateDataFrameResponse.getMetrics().size(), equalTo(4)); { // Accuracy Accuracy.Result accuracyResult = (Accuracy.Result) evaluateDataFrameResponse.getMetrics().get(0); @@ -483,6 +487,24 @@ private void assertEvaluation(String dependentVariable, List dependentVar } assertThat(confusionMatrixResult.getOtherActualClassCount(), equalTo(0L)); } + + { // Precision + Precision.Result precisionResult = (Precision.Result) evaluateDataFrameResponse.getMetrics().get(2); + assertThat(precisionResult.getMetricName(), equalTo(Precision.NAME.getPreferredName())); + for (Precision.PerClassResult klass : precisionResult.getClasses()) { + assertThat(klass.getClassName(), is(in(dependentVariableValuesAsStrings))); + assertThat(klass.getPrecision(), allOf(greaterThanOrEqualTo(0.0), lessThanOrEqualTo(1.0))); + } + } + + { // Recall + Recall.Result recallResult = (Recall.Result) evaluateDataFrameResponse.getMetrics().get(3); + assertThat(recallResult.getMetricName(), equalTo(Recall.NAME.getPreferredName())); + for (Recall.PerClassResult klass : recallResult.getClasses()) { + assertThat(klass.getClassName(), is(in(dependentVariableValuesAsStrings))); + assertThat(klass.getRecall(), allOf(greaterThanOrEqualTo(0.0), lessThanOrEqualTo(1.0))); + } + } } protected String stateDocId() { diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/evaluate_data_frame.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/evaluate_data_frame.yml index 9d0d645e3d33a..95a7ef4e33218 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/evaluate_data_frame.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/evaluate_data_frame.yml @@ -632,6 +632,58 @@ setup: accuracy: 0.5 # 1 out of 2 overall_accuracy: 0.625 # 5 out of 8 --- +"Test classification precision": + - do: + ml.evaluate_data_frame: + body: > + { + "index": "utopia", + "evaluation": { + "classification": { + "actual_field": "classification_field_act.keyword", + "predicted_field": "classification_field_pred.keyword", + "metrics": { "precision": {} } + } + } + } + + - match: + classification.precision: + classes: + - class_name: "cat" + precision: 0.5 # 2 out of 4 + - class_name: "dog" + precision: 0.6666666666666666 # 2 out of 3 + - class_name: "mouse" + precision: 1.0 # 1 out of 1 + avg_precision: 0.7222222222222222 +--- +"Test classification recall": + - do: + ml.evaluate_data_frame: + body: > + { + "index": "utopia", + "evaluation": { + "classification": { + "actual_field": "classification_field_act.keyword", + "predicted_field": "classification_field_pred.keyword", + "metrics": { "recall": {} } + } + } + } + + - match: + classification.recall: + classes: + - class_name: "cat" + recall: 0.6666666666666666 # 2 out of 3 + - class_name: "dog" + recall: 0.6666666666666666 # 2 out of 3 + - class_name: "mouse" + recall: 0.5 # 1 out of 2 + avg_recall: 0.611111111111111 +--- "Test classification multiclass_confusion_matrix": - do: ml.evaluate_data_frame: From 845935d382fc6d6c4e10e183ac1f28be5d9fe0a4 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Thu, 19 Dec 2019 15:11:34 +0000 Subject: [PATCH 272/686] Reduce the max depth of randomly generated interval queries (#50317) We randomly generate intervals sources to test serialization and query generation in IntervalQueryBuilderTests. However, rarely we can generate a query that has too many nested disjunctions, resulting in query rewrites running afoul of the maximum boolean clause limit. This commit reduces the maximum depth of the randomly generated intervals source to make running into this limit much more unlikely. --- .../elasticsearch/index/query/IntervalQueryBuilderTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java index ed28a06500d5a..cfcc1a9cd3eb0 100644 --- a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java @@ -86,7 +86,7 @@ protected void initializeAdditionalMappings(MapperService mapperService) throws } private IntervalsSourceProvider createRandomSource(int depth, boolean useScripts) { - if (depth > 3) { + if (depth > 2) { return createRandomMatch(depth + 1, useScripts); } switch (randomInt(20)) { From 911e3318daefe569b99535edebc0c717527960cd Mon Sep 17 00:00:00 2001 From: Aleksandr Maus Date: Thu, 19 Dec 2019 11:14:54 -0500 Subject: [PATCH 273/686] Improve SearchHit "equals" implementation for null fields cases (#50327) * Improve SearchHit "equals" implementation for null fields cases --- .../main/java/org/elasticsearch/search/SearchHit.java | 2 +- .../java/org/elasticsearch/search/SearchHitTests.java | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/SearchHit.java b/server/src/main/java/org/elasticsearch/search/SearchHit.java index 23fb3d8d628b2..209e1e1b78c74 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchHit.java +++ b/server/src/main/java/org/elasticsearch/search/SearchHit.java @@ -887,7 +887,7 @@ public boolean equals(Object obj) { && Objects.equals(seqNo, other.seqNo) && Objects.equals(primaryTerm, other.primaryTerm) && Objects.equals(source, other.source) - && Objects.equals(fields, other.fields) + && Objects.equals(getFields(), other.getFields()) && Objects.equals(getHighlightFields(), other.getHighlightFields()) && Arrays.equals(matchedQueries, other.matchedQueries) && Objects.equals(explanation, other.explanation) diff --git a/server/src/test/java/org/elasticsearch/search/SearchHitTests.java b/server/src/test/java/org/elasticsearch/search/SearchHitTests.java index 5d5d26f489677..0fba6141c7363 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchHitTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchHitTests.java @@ -71,9 +71,12 @@ public static SearchHit createTestItem(XContentType xContentType, boolean withOp if (randomBoolean()) { nestedIdentity = NestedIdentityTests.createTestItem(randomIntBetween(0, 2)); } - Map fields = new HashMap<>(); - if (randomBoolean()) { - fields = GetResultTests.randomDocumentFields(xContentType).v2(); + Map fields = null; + if (frequently()) { + fields = new HashMap<>(); + if (randomBoolean()) { + fields = GetResultTests.randomDocumentFields(xContentType).v2(); + } } SearchHit hit = new SearchHit(internalId, uid, nestedIdentity, fields); if (frequently()) { From f85f7696a5bb74e3e3a170ffa6c2c32f820f57d3 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Thu, 19 Dec 2019 09:31:28 -0700 Subject: [PATCH 274/686] Rename the remote connection mode simple to proxy (#50291) This commit renames the simple connection mode to the proxy connection mode for remote cluster connections. In order to do this, the mode specific settings which we namespaced by their mode (ex: sniff.seed and proxy.addresses) have been reverted. --- .../java/org/elasticsearch/client/CCRIT.java | 2 +- .../documentation/CCRDocumentationIT.java | 2 +- ...rossClusterSearchUnavailableClusterIT.java | 16 ++-- .../15_connection_mode_configuration.yml | 84 +++++++++---------- .../test/multi_cluster/20_info.yml | 30 +++---- .../common/settings/ClusterSettings.java | 9 +- ...tegy.java => ProxyConnectionStrategy.java} | 68 +++++++-------- .../transport/RemoteClusterAware.java | 7 +- .../transport/RemoteConnectionStrategy.java | 12 +-- .../transport/SniffConnectionStrategy.java | 24 +----- .../search/TransportSearchActionTests.java | 2 +- ...java => ProxyConnectionStrategyTests.java} | 44 +++++----- .../RemoteClusterAwareClientTests.java | 4 +- .../transport/RemoteClusterClientTests.java | 4 +- .../RemoteClusterConnectionTests.java | 23 ++--- .../transport/RemoteClusterServiceTests.java | 56 ++++++------- .../transport/RemoteClusterSettingsTests.java | 4 +- .../RemoteConnectionStrategyTests.java | 16 ++-- .../SniffConnectionStrategyTests.java | 7 +- .../xpack/ccr/IndexFollowingIT.java | 2 +- .../70_connection_mode_configuration.yml | 84 +++++++++---------- 21 files changed, 234 insertions(+), 266 deletions(-) rename server/src/main/java/org/elasticsearch/transport/{SimpleConnectionStrategy.java => ProxyConnectionStrategy.java} (84%) rename server/src/test/java/org/elasticsearch/transport/{SimpleConnectionStrategyTests.java => ProxyConnectionStrategyTests.java} (91%) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java index 7f817103a63fc..6be2efe98c48b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java @@ -83,7 +83,7 @@ public void setupRemoteClusterConfig() throws Exception { String transportAddress = (String) nodesResponse.get("transport_address"); ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); - updateSettingsRequest.transientSettings(Collections.singletonMap("cluster.remote.local_cluster.sniff.seeds", transportAddress)); + updateSettingsRequest.transientSettings(Collections.singletonMap("cluster.remote.local_cluster.seeds", transportAddress)); ClusterUpdateSettingsResponse updateSettingsResponse = highLevelClient().cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); assertThat(updateSettingsResponse.isAcknowledged(), is(true)); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java index 465a035952599..48f7adda8445c 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java @@ -87,7 +87,7 @@ public void setupRemoteClusterConfig() throws IOException { String transportAddress = (String) nodesResponse.get("transport_address"); ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); - updateSettingsRequest.transientSettings(Collections.singletonMap("cluster.remote.local.sniff.seeds", transportAddress)); + updateSettingsRequest.transientSettings(Collections.singletonMap("cluster.remote.local.seeds", transportAddress)); ClusterUpdateSettingsResponse updateSettingsResponse = client.cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); assertThat(updateSettingsResponse.isAcknowledged(), is(true)); diff --git a/qa/ccs-unavailable-clusters/src/test/java/org/elasticsearch/search/CrossClusterSearchUnavailableClusterIT.java b/qa/ccs-unavailable-clusters/src/test/java/org/elasticsearch/search/CrossClusterSearchUnavailableClusterIT.java index 7b2a4835d1964..2bf0eae138135 100644 --- a/qa/ccs-unavailable-clusters/src/test/java/org/elasticsearch/search/CrossClusterSearchUnavailableClusterIT.java +++ b/qa/ccs-unavailable-clusters/src/test/java/org/elasticsearch/search/CrossClusterSearchUnavailableClusterIT.java @@ -144,7 +144,7 @@ public void testSearchSkipUnavailable() throws IOException { try (MockTransportService remoteTransport = startTransport("node0", new CopyOnWriteArrayList<>(), Version.CURRENT, threadPool)) { DiscoveryNode remoteNode = remoteTransport.getLocalDiscoNode(); - updateRemoteClusterSettings(Collections.singletonMap("sniff.seeds", remoteNode.getAddress().toString())); + updateRemoteClusterSettings(Collections.singletonMap("seeds", remoteNode.getAddress().toString())); for (int i = 0; i < 10; i++) { restHighLevelClient.index( @@ -229,7 +229,7 @@ public void testSearchSkipUnavailable() throws IOException { assertSearchConnectFailure(); Map map = new HashMap<>(); - map.put("sniff.seeds", null); + map.put("seeds", null); map.put("skip_unavailable", null); updateRemoteClusterSettings(map); } @@ -248,32 +248,32 @@ public void testSkipUnavailableDependsOnSeeds() throws IOException { () -> client().performRequest(request)); assertEquals(400, responseException.getResponse().getStatusLine().getStatusCode()); assertThat(responseException.getMessage(), - containsString("missing required setting [cluster.remote.remote1.sniff.seeds] " + + containsString("missing required setting [cluster.remote.remote1.seeds] " + "for setting [cluster.remote.remote1.skip_unavailable]")); } Map settingsMap = new HashMap<>(); - settingsMap.put("sniff.seeds", remoteNode.getAddress().toString()); + settingsMap.put("seeds", remoteNode.getAddress().toString()); settingsMap.put("skip_unavailable", randomBoolean()); updateRemoteClusterSettings(settingsMap); { //check that seeds cannot be reset alone if skip_unavailable is set Request request = new Request("PUT", "/_cluster/settings"); - request.setEntity(buildUpdateSettingsRequestBody(Collections.singletonMap("sniff.seeds", null))); + request.setEntity(buildUpdateSettingsRequestBody(Collections.singletonMap("seeds", null))); ResponseException responseException = expectThrows(ResponseException.class, () -> client().performRequest(request)); assertEquals(400, responseException.getResponse().getStatusLine().getStatusCode()); - assertThat(responseException.getMessage(), containsString("missing required setting [cluster.remote.remote1.sniff.seeds] " + + assertThat(responseException.getMessage(), containsString("missing required setting [cluster.remote.remote1.seeds] " + "for setting [cluster.remote.remote1.skip_unavailable]")); } if (randomBoolean()) { updateRemoteClusterSettings(Collections.singletonMap("skip_unavailable", null)); - updateRemoteClusterSettings(Collections.singletonMap("sniff.seeds", null)); + updateRemoteClusterSettings(Collections.singletonMap("seeds", null)); } else { Map nullMap = new HashMap<>(); - nullMap.put("sniff.seeds", null); + nullMap.put("seeds", null); nullMap.put("skip_unavailable", null); updateRemoteClusterSettings(nullMap); } diff --git a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/15_connection_mode_configuration.yml b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/15_connection_mode_configuration.yml index ed639b3655ed5..5606a08cd261e 100644 --- a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/15_connection_mode_configuration.yml +++ b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/15_connection_mode_configuration.yml @@ -1,5 +1,5 @@ --- -"Add transient remote cluster in simple mode with invalid sniff settings": +"Add transient remote cluster in proxy mode with invalid sniff settings": - do: cluster.get_settings: include_defaults: true @@ -12,14 +12,14 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.sniff.node_connections: "5" - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.node_connections: "5" + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.sniff.node_connections\" cannot be - used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=SIMPLE]" } + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.node_connections\" cannot be + used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=PROXY]" } - do: catch: bad_request @@ -27,17 +27,17 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.seeds: $remote_ip + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.sniff.seeds\" cannot be - used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=SIMPLE]" } + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.seeds\" cannot be + used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=PROXY]" } --- -"Add transient remote cluster in sniff mode with invalid simple settings": +"Add transient remote cluster in sniff mode with invalid proxy settings": - do: cluster.get_settings: include_defaults: true @@ -50,13 +50,13 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.simple.socket_connections: "20" - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip + cluster.remote.test_remote_cluster.proxy_socket_connections: "20" + cluster.remote.test_remote_cluster.seeds: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.simple.socket_connections\" cannot be - used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SIMPLE, configured=SNIFF]" } + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.proxy_socket_connections\" cannot be + used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=PROXY, configured=SNIFF]" } - do: catch: bad_request @@ -64,16 +64,16 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.seeds: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.simple.addresses\" cannot be - used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SIMPLE, configured=SNIFF]" } + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.proxy_addresses\" cannot be + used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=PROXY, configured=SNIFF]" } --- -"Add transient remote cluster using simple connection mode using valid settings": +"Add transient remote cluster using proxy connection mode using valid settings": - do: cluster.get_settings: include_defaults: true @@ -85,13 +85,13 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.simple.socket_connections: "3" - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.proxy_socket_connections: "3" + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "simple"} - - match: {transient.cluster\.remote\.test_remote_cluster\.simple\.socket_connections: "3"} - - match: {transient.cluster\.remote\.test_remote_cluster\.simple\.addresses: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "proxy"} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_socket_connections: "3"} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_addresses: $remote_ip} - do: search: @@ -120,12 +120,12 @@ body: transient: cluster.remote.test_remote_cluster.mode: "sniff" - cluster.remote.test_remote_cluster.sniff.node_connections: "3" - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip + cluster.remote.test_remote_cluster.node_connections: "3" + cluster.remote.test_remote_cluster.seeds: $remote_ip - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "sniff"} - - match: {transient.cluster\.remote\.test_remote_cluster\.sniff\.node_connections: "3"} - - match: {transient.cluster\.remote\.test_remote_cluster\.sniff\.seeds: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.node_connections: "3"} + - match: {transient.cluster\.remote\.test_remote_cluster\.seeds: $remote_ip} - do: search: @@ -154,10 +154,10 @@ body: transient: cluster.remote.test_remote_cluster.mode: "sniff" - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip + cluster.remote.test_remote_cluster.seeds: $remote_ip - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "sniff"} - - match: {transient.cluster\.remote\.test_remote_cluster\.sniff\.seeds: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.seeds: $remote_ip} - do: search: @@ -178,25 +178,25 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.sniff.seeds\" cannot be - used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=SIMPLE]" } + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.seeds\" cannot be + used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=PROXY]" } - do: cluster.put_settings: flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.sniff.seeds: null - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.seeds: null + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "simple"} - - match: {transient.cluster\.remote\.test_remote_cluster\.simple\.addresses: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "proxy"} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_addresses: $remote_ip} - do: search: diff --git a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml index 761526a7bea60..10378aaeda125 100644 --- a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml +++ b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml @@ -23,12 +23,12 @@ body: transient: cluster.remote.test_remote_cluster.mode: "sniff" - cluster.remote.test_remote_cluster.sniff.node_connections: "2" - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip + cluster.remote.test_remote_cluster.node_connections: "2" + cluster.remote.test_remote_cluster.seeds: $remote_ip - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "sniff"} - - match: {transient.cluster\.remote\.test_remote_cluster\.sniff\.node_connections: "2"} - - match: {transient.cluster\.remote\.test_remote_cluster\.sniff\.seeds: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.node_connections: "2"} + - match: {transient.cluster\.remote\.test_remote_cluster\.seeds: $remote_ip} # we do another search here since this will enforce the connection to be established # otherwise the cluster might not have been connected yet. @@ -66,15 +66,15 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.sniff.seeds: null - cluster.remote.test_remote_cluster.sniff.node_connections: null - cluster.remote.test_remote_cluster.simple.socket_connections: "10" - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.seeds: null + cluster.remote.test_remote_cluster.node_connections: null + cluster.remote.test_remote_cluster.proxy_socket_connections: "10" + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "simple"} - - match: {transient.cluster\.remote\.test_remote_cluster\.simple\.socket_connections: "10"} - - match: {transient.cluster\.remote\.test_remote_cluster\.simple\.addresses: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "proxy"} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_socket_connections: "10"} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_addresses: $remote_ip} - do: cluster.remote_info: {} @@ -84,15 +84,15 @@ - gt: { test_remote_cluster.num_sockets_connected: 0} - match: { test_remote_cluster.max_socket_connections: 10} - match: { test_remote_cluster.initial_connect_timeout: "30s" } - - match: { test_remote_cluster.mode: "simple" } + - match: { test_remote_cluster.mode: "proxy" } - do: cluster.put_settings: body: transient: cluster.remote.test_remote_cluster.mode: null - cluster.remote.test_remote_cluster.simple.socket_connections: null - cluster.remote.test_remote_cluster.simple.addresses: null + cluster.remote.test_remote_cluster.proxy_socket_connections: null + cluster.remote.test_remote_cluster.proxy_addresses: null --- "skip_unavailable is returned as part of _remote/info response": diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 694c1e2bddf64..928de4a0cb5e3 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -104,7 +104,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.RemoteClusterService; import org.elasticsearch.transport.RemoteConnectionStrategy; -import org.elasticsearch.transport.SimpleConnectionStrategy; +import org.elasticsearch.transport.ProxyConnectionStrategy; import org.elasticsearch.transport.SniffConnectionStrategy; import org.elasticsearch.transport.TransportSettings; import org.elasticsearch.watcher.ResourceWatcherService; @@ -293,10 +293,9 @@ public void apply(Settings value, Settings current, Settings previous) { RemoteClusterService.REMOTE_CLUSTER_PING_SCHEDULE, RemoteClusterService.REMOTE_CLUSTER_COMPRESS, RemoteConnectionStrategy.REMOTE_CONNECTION_MODE, - SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES, - SimpleConnectionStrategy.REMOTE_SOCKET_CONNECTIONS, - SimpleConnectionStrategy.INCLUDE_SERVER_NAME, - SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD, + ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES, + ProxyConnectionStrategy.REMOTE_SOCKET_CONNECTIONS, + ProxyConnectionStrategy.INCLUDE_SERVER_NAME, SniffConnectionStrategy.REMOTE_CLUSTERS_PROXY, SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS, SniffConnectionStrategy.REMOTE_NODE_CONNECTIONS, diff --git a/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java similarity index 84% rename from server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java rename to server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java index 9393e4d0cafc0..dabdd771afaed 100644 --- a/server/src/main/java/org/elasticsearch/transport/SimpleConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java @@ -54,7 +54,7 @@ import static org.elasticsearch.common.settings.Setting.boolSetting; import static org.elasticsearch.common.settings.Setting.intSetting; -public class SimpleConnectionStrategy extends RemoteConnectionStrategy { +public class ProxyConnectionStrategy extends RemoteConnectionStrategy { /** * A list of addresses for remote cluster connections. The connections will be opened to the configured addresses in a round-robin @@ -62,12 +62,12 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { */ public static final Setting.AffixSetting> REMOTE_CLUSTER_ADDRESSES = Setting.affixKeySetting( "cluster.remote.", - "simple.addresses", + "proxy_addresses", (ns, key) -> Setting.listSetting(key, Collections.emptyList(), s -> { // validate address parsePort(s); return s; - }, new StrategyValidator<>(ns, key, ConnectionStrategy.SIMPLE), + }, new StrategyValidator<>(ns, key, ConnectionStrategy.PROXY), Setting.Property.Dynamic, Setting.Property.NodeScope)); /** @@ -75,8 +75,8 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { */ public static final Setting.AffixSetting REMOTE_SOCKET_CONNECTIONS = Setting.affixKeySetting( "cluster.remote.", - "simple.socket_connections", - (ns, key) -> intSetting(key, 18, 1, new StrategyValidator<>(ns, key, ConnectionStrategy.SIMPLE), + "proxy_socket_connections", + (ns, key) -> intSetting(key, 18, 1, new StrategyValidator<>(ns, key, ConnectionStrategy.PROXY), Setting.Property.Dynamic, Setting.Property.NodeScope)); /** @@ -84,14 +84,14 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { */ public static final Setting.AffixSetting INCLUDE_SERVER_NAME = Setting.affixKeySetting( "cluster.remote.", - "simple.include_server_name", - (ns, key) -> boolSetting(key, false, new StrategyValidator<>(ns, key, ConnectionStrategy.SIMPLE), + "include_server_name", + (ns, key) -> boolSetting(key, false, new StrategyValidator<>(ns, key, ConnectionStrategy.PROXY), Setting.Property.Dynamic, Setting.Property.NodeScope)); static final int CHANNELS_PER_CONNECTION = 1; private static final int MAX_CONNECT_ATTEMPTS_PER_RUN = 3; - private static final Logger logger = LogManager.getLogger(SimpleConnectionStrategy.class); + private static final Logger logger = LogManager.getLogger(ProxyConnectionStrategy.class); private final int maxNumConnections; private final AtomicLong counter = new AtomicLong(0); @@ -102,8 +102,8 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { private final ConnectionProfile profile; private final ConnectionManager.ConnectionValidator clusterNameValidator; - SimpleConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, - Settings settings) { + ProxyConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, + Settings settings) { this( clusterAlias, transportService, @@ -113,28 +113,28 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { INCLUDE_SERVER_NAME.getConcreteSettingForNamespace(clusterAlias).get(settings)); } - SimpleConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, - int maxNumConnections, List configuredAddresses) { + ProxyConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, + int maxNumConnections, List configuredAddresses) { this(clusterAlias, transportService, connectionManager, maxNumConnections, configuredAddresses, configuredAddresses.stream().map(address -> (Supplier) () -> resolveAddress(address)).collect(Collectors.toList()), false); } - SimpleConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, - int maxNumConnections, List configuredAddresses, boolean includeServerName) { + ProxyConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, + int maxNumConnections, List configuredAddresses, boolean includeServerName) { this(clusterAlias, transportService, connectionManager, maxNumConnections, configuredAddresses, configuredAddresses.stream().map(address -> (Supplier) () -> resolveAddress(address)).collect(Collectors.toList()), includeServerName); } - SimpleConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, - int maxNumConnections, List configuredAddresses, List> addresses, - boolean includeServerName) { + ProxyConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, + int maxNumConnections, List configuredAddresses, List> addresses, + boolean includeServerName) { super(clusterAlias, transportService, connectionManager); this.maxNumConnections = maxNumConnections; this.configuredAddresses = configuredAddresses; this.includeServerName = includeServerName; - assert addresses.isEmpty() == false : "Cannot use simple connection strategy with no configured addresses"; + assert addresses.isEmpty() == false : "Cannot use proxy connection strategy with no configured addresses"; this.addresses = addresses; // TODO: Move into the ConnectionManager this.profile = new ConnectionProfile.Builder() @@ -158,11 +158,11 @@ public class SimpleConnectionStrategy extends RemoteConnectionStrategy { } static Stream> enablementSettings() { - return Stream.of(SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES); + return Stream.of(ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES); } static Writeable.Reader infoReader() { - return SimpleModeInfo::new; + return ProxyModeInfo::new; } @Override @@ -179,20 +179,20 @@ protected boolean strategyMustBeRebuilt(Settings newSettings) { @Override protected ConnectionStrategy strategyType() { - return ConnectionStrategy.SIMPLE; + return ConnectionStrategy.PROXY; } @Override protected void connectImpl(ActionListener listener) { - performSimpleConnectionProcess(listener); + performProxyConnectionProcess(listener); } @Override public RemoteConnectionInfo.ModeInfo getModeInfo() { - return new SimpleModeInfo(configuredAddresses, maxNumConnections, connectionManager.size()); + return new ProxyModeInfo(configuredAddresses, maxNumConnections, connectionManager.size()); } - private void performSimpleConnectionProcess(ActionListener listener) { + private void performProxyConnectionProcess(ActionListener listener) { openConnections(listener, 1); } @@ -256,7 +256,7 @@ public void onFailure(Exception e) { } else { int openConnections = connectionManager.size(); if (openConnections == 0) { - finished.onFailure(new IllegalStateException("Unable to open any simple connections to remote cluster [" + clusterAlias + finished.onFailure(new IllegalStateException("Unable to open any proxy connections to remote cluster [" + clusterAlias + "]")); } else { logger.debug("unable to open maximum number of connections [remote cluster: {}, opened: {}, maximum: {}]", clusterAlias, @@ -285,19 +285,19 @@ private boolean addressesChanged(final List oldAddresses, final List addresses; private final int maxSocketConnections; private final int numSocketsConnected; - SimpleModeInfo(List addresses, int maxSocketConnections, int numSocketsConnected) { + ProxyModeInfo(List addresses, int maxSocketConnections, int numSocketsConnected) { this.addresses = addresses; this.maxSocketConnections = maxSocketConnections; this.numSocketsConnected = numSocketsConnected; } - private SimpleModeInfo(StreamInput input) throws IOException { + private ProxyModeInfo(StreamInput input) throws IOException { addresses = Arrays.asList(input.readStringArray()); maxSocketConnections = input.readVInt(); numSocketsConnected = input.readVInt(); @@ -329,22 +329,22 @@ public boolean isConnected() { @Override public String modeName() { - return "simple"; + return "proxy"; } @Override public RemoteConnectionStrategy.ConnectionStrategy modeType() { - return RemoteConnectionStrategy.ConnectionStrategy.SIMPLE; + return RemoteConnectionStrategy.ConnectionStrategy.PROXY; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - SimpleModeInfo simple = (SimpleModeInfo) o; - return maxSocketConnections == simple.maxSocketConnections && - numSocketsConnected == simple.numSocketsConnected && - Objects.equals(addresses, simple.addresses); + ProxyModeInfo otherProxy = (ProxyModeInfo) o; + return maxSocketConnections == otherProxy.maxSocketConnections && + numSocketsConnected == otherProxy.numSocketsConnected && + Objects.equals(addresses, otherProxy.addresses); } @Override diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java index 63a9b857f5f7d..ba4f98d0d09a3 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java @@ -108,11 +108,10 @@ public void listenForUpdates(ClusterSettings clusterSettings) { RemoteConnectionStrategy.REMOTE_CONNECTION_MODE, SniffConnectionStrategy.REMOTE_CLUSTERS_PROXY, SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS, - SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD, SniffConnectionStrategy.REMOTE_NODE_CONNECTIONS, - SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES, - SimpleConnectionStrategy.REMOTE_SOCKET_CONNECTIONS, - SimpleConnectionStrategy.INCLUDE_SERVER_NAME); + ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES, + ProxyConnectionStrategy.REMOTE_SOCKET_CONNECTIONS, + ProxyConnectionStrategy.INCLUDE_SERVER_NAME); clusterSettings.addAffixGroupUpdateConsumer(remoteClusterSettings, this::validateAndUpdateRemoteCluster); } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java index addd68b1c535e..562a6bfbe464f 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java @@ -65,11 +65,11 @@ public String toString() { return "sniff"; } }, - SIMPLE(SimpleConnectionStrategy.CHANNELS_PER_CONNECTION, SimpleConnectionStrategy::enablementSettings, - SimpleConnectionStrategy::infoReader) { + PROXY(ProxyConnectionStrategy.CHANNELS_PER_CONNECTION, ProxyConnectionStrategy::enablementSettings, + ProxyConnectionStrategy::infoReader) { @Override public String toString() { - return "simple"; + return "proxy"; } }; @@ -140,8 +140,8 @@ static RemoteConnectionStrategy buildStrategy(String clusterAlias, TransportServ switch (mode) { case SNIFF: return new SniffConnectionStrategy(clusterAlias, transportService, connectionManager, settings); - case SIMPLE: - return new SimpleConnectionStrategy(clusterAlias, transportService, connectionManager, settings); + case PROXY: + return new ProxyConnectionStrategy(clusterAlias, transportService, connectionManager, settings); default: throw new AssertionError("Invalid connection strategy" + mode); } @@ -159,7 +159,7 @@ public static boolean isConnectionEnabled(String clusterAlias, Settings settings List seeds = SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS.getConcreteSettingForNamespace(clusterAlias).get(settings); return seeds.isEmpty() == false; } else { - List addresses = SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias) + List addresses = ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias) .get(settings); return addresses.isEmpty() == false; } diff --git a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java index bb7d6202c59b7..97b5e318841d6 100644 --- a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java @@ -68,7 +68,7 @@ public class SniffConnectionStrategy extends RemoteConnectionStrategy { /** * A list of initial seed nodes to discover eligible nodes from the remote cluster */ - public static final Setting.AffixSetting> REMOTE_CLUSTER_SEEDS_OLD = Setting.affixKeySetting( + public static final Setting.AffixSetting> REMOTE_CLUSTER_SEEDS = Setting.affixKeySetting( "cluster.remote.", "seeds", (ns, key) -> Setting.listSetting( @@ -83,24 +83,6 @@ public class SniffConnectionStrategy extends RemoteConnectionStrategy { Setting.Property.Dynamic, Setting.Property.NodeScope)); - /** - * A list of initial seed nodes to discover eligible nodes from the remote cluster - */ - public static final Setting.AffixSetting> REMOTE_CLUSTER_SEEDS = Setting.affixKeySetting( - "cluster.remote.", - "sniff.seeds", - (ns, key) -> Setting.listSetting(key, - REMOTE_CLUSTER_SEEDS_OLD.getConcreteSettingForNamespace(ns), - s -> { - // validate seed address - parsePort(s); - return s; - }, - s -> REMOTE_CLUSTER_SEEDS_OLD.getConcreteSettingForNamespace(ns).get(s), - new StrategyValidator<>(ns, key, ConnectionStrategy.SNIFF), - Setting.Property.Dynamic, - Setting.Property.NodeScope)); - /** * A proxy address for the remote cluster. By default this is not set, meaning that Elasticsearch will connect directly to the nodes in @@ -138,7 +120,7 @@ public class SniffConnectionStrategy extends RemoteConnectionStrategy { */ public static final Setting.AffixSetting REMOTE_NODE_CONNECTIONS = Setting.affixKeySetting( "cluster.remote.", - "sniff.node_connections", + "node_connections", (ns, key) -> intSetting( key, REMOTE_CONNECTIONS_PER_CLUSTER, @@ -194,7 +176,7 @@ public class SniffConnectionStrategy extends RemoteConnectionStrategy { } static Stream> enablementSettings() { - return Stream.of(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS, SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD); + return Stream.of(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS); } static Writeable.Reader infoReader() { diff --git a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java index e9aeff6847ad5..105bcc66ac1c3 100644 --- a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java @@ -351,7 +351,7 @@ private MockTransportService[] startTransport(int numClusters, DiscoveryNode[] n DiscoveryNode remoteSeedNode = remoteSeedTransport.getLocalDiscoNode(); knownNodes.add(remoteSeedNode); nodes[i] = remoteSeedNode; - settingsBuilder.put("cluster.remote.remote" + i + ".sniff.seeds", remoteSeedNode.getAddress().toString()); + settingsBuilder.put("cluster.remote.remote" + i + ".seeds", remoteSeedNode.getAddress().toString()); remoteIndices.put("remote" + i, new OriginalIndices(new String[]{"index"}, IndicesOptions.lenientExpandOpen())); } return mockTransportServices; diff --git a/server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java b/server/src/test/java/org/elasticsearch/transport/ProxyConnectionStrategyTests.java similarity index 91% rename from server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java rename to server/src/test/java/org/elasticsearch/transport/ProxyConnectionStrategyTests.java index 53fe4a04d379c..5644ff895725d 100644 --- a/server/src/test/java/org/elasticsearch/transport/SimpleConnectionStrategyTests.java +++ b/server/src/test/java/org/elasticsearch/transport/ProxyConnectionStrategyTests.java @@ -48,11 +48,11 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -public class SimpleConnectionStrategyTests extends ESTestCase { +public class ProxyConnectionStrategyTests extends ESTestCase { private final String clusterAlias = "cluster-alias"; private final String modeKey = RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace(clusterAlias).getKey(); - private final Settings settings = Settings.builder().put(modeKey, "simple").build(); + private final Settings settings = Settings.builder().put(modeKey, "proxy").build(); private final ConnectionProfile profile = RemoteConnectionStrategy.buildConnectionProfile("cluster", settings); private final ThreadPool threadPool = new TestThreadPool(getClass().getName()); @@ -86,7 +86,7 @@ public MockTransportService startTransport(final String id, final Version versio } } - public void testSimpleStrategyWillOpenExpectedNumberOfConnectionsToAddresses() { + public void testProxyStrategyWillOpenExpectedNumberOfConnectionsToAddresses() { try (MockTransportService transport1 = startTransport("node1", Version.CURRENT); MockTransportService transport2 = startTransport("node2", Version.CURRENT)) { TransportAddress address1 = transport1.boundAddress().publishAddress(); @@ -99,7 +99,7 @@ public void testSimpleStrategyWillOpenExpectedNumberOfConnectionsToAddresses() { ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); - SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, + ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, numOfConnections, addresses(address1, address2))) { assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); @@ -117,7 +117,7 @@ numOfConnections, addresses(address1, address2))) { } } - public void testSimpleStrategyWillOpenNewConnectionsOnDisconnect() throws Exception { + public void testProxyStrategyWillOpenNewConnectionsOnDisconnect() throws Exception { try (MockTransportService transport1 = startTransport("node1", Version.CURRENT); MockTransportService transport2 = startTransport("node2", Version.CURRENT)) { TransportAddress address1 = transport1.boundAddress().publishAddress(); @@ -130,7 +130,7 @@ public void testSimpleStrategyWillOpenNewConnectionsOnDisconnect() throws Except ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); - SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, + ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, numOfConnections, addresses(address1, address2))) { assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); @@ -191,7 +191,7 @@ public void testConnectWithSingleIncompatibleNode() { }); int numOfConnections = 5; try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); - SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, + ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, numOfConnections, addresses(address1, address2))) { assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); @@ -225,7 +225,7 @@ public void testConnectFailsWithIncompatibleNodes() { ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); - SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, + ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, numOfConnections, addresses(address1))) { PlainActionFuture connectFuture = PlainActionFuture.newFuture(); @@ -255,7 +255,7 @@ public void testClusterNameValidationPreventConnectingToDifferentClusters() thro ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); - SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, + ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, numOfConnections, addresses(address1, address2))) { assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); @@ -275,7 +275,7 @@ numOfConnections, addresses(address1, address2))) { } } - public void testSimpleStrategyWillResolveAddressesEachConnect() throws Exception { + public void testProxyStrategyWillResolveAddressesEachConnect() throws Exception { try (MockTransportService transport1 = startTransport("seed_node", Version.CURRENT)) { TransportAddress address = transport1.boundAddress().publishAddress(); @@ -292,7 +292,7 @@ public void testSimpleStrategyWillResolveAddressesEachConnect() throws Exception ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); - SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, + ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, numOfConnections, addresses(address), Collections.singletonList(addressSupplier), false)) { PlainActionFuture connectFuture = PlainActionFuture.newFuture(); strategy.connect(connectFuture); @@ -306,7 +306,7 @@ numOfConnections, addresses(address), Collections.singletonList(addressSupplier } } - public void testSimpleStrategyWillNeedToBeRebuiltIfNumOfSocketsOrAddressesChange() { + public void testProxyStrategyWillNeedToBeRebuiltIfNumOfSocketsOrAddressesChange() { try (MockTransportService transport1 = startTransport("node1", Version.CURRENT); MockTransportService transport2 = startTransport("node2", Version.CURRENT)) { TransportAddress address1 = transport1.boundAddress().publishAddress(); @@ -319,7 +319,7 @@ public void testSimpleStrategyWillNeedToBeRebuiltIfNumOfSocketsOrAddressesChange ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); - SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, + ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, numOfConnections, addresses(address1, address2))) { PlainActionFuture connectFuture = PlainActionFuture.newFuture(); strategy.connect(connectFuture); @@ -332,24 +332,24 @@ numOfConnections, addresses(address1, address2))) { Setting modeSetting = RemoteConnectionStrategy.REMOTE_CONNECTION_MODE .getConcreteSettingForNamespace("cluster-alias"); - Setting addressesSetting = SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES + Setting addressesSetting = ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES .getConcreteSettingForNamespace("cluster-alias"); - Setting socketConnections = SimpleConnectionStrategy.REMOTE_SOCKET_CONNECTIONS + Setting socketConnections = ProxyConnectionStrategy.REMOTE_SOCKET_CONNECTIONS .getConcreteSettingForNamespace("cluster-alias"); Settings noChange = Settings.builder() - .put(modeSetting.getKey(), "simple") + .put(modeSetting.getKey(), "proxy") .put(addressesSetting.getKey(), Strings.arrayToCommaDelimitedString(addresses(address1, address2).toArray())) .put(socketConnections.getKey(), numOfConnections) .build(); assertFalse(strategy.shouldRebuildConnection(noChange)); Settings addressesChanged = Settings.builder() - .put(modeSetting.getKey(), "simple") + .put(modeSetting.getKey(), "proxy") .put(addressesSetting.getKey(), Strings.arrayToCommaDelimitedString(addresses(address1).toArray())) .build(); assertTrue(strategy.shouldRebuildConnection(addressesChanged)); Settings socketsChanged = Settings.builder() - .put(modeSetting.getKey(), "simple") + .put(modeSetting.getKey(), "proxy") .put(addressesSetting.getKey(), Strings.arrayToCommaDelimitedString(addresses(address1, address2).toArray())) .put(socketConnections.getKey(), numOfConnections + 1) .build(); @@ -361,8 +361,8 @@ numOfConnections, addresses(address1, address2))) { public void testModeSettingsCannotBeUsedWhenInDifferentMode() { List, String>> restrictedSettings = Arrays.asList( - new Tuple<>(SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES, "192.168.0.1:8080"), - new Tuple<>(SimpleConnectionStrategy.REMOTE_SOCKET_CONNECTIONS, "3")); + new Tuple<>(ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES, "192.168.0.1:8080"), + new Tuple<>(ProxyConnectionStrategy.REMOTE_SOCKET_CONNECTIONS, "3")); RemoteConnectionStrategy.ConnectionStrategy sniff = RemoteConnectionStrategy.ConnectionStrategy.SNIFF; @@ -384,7 +384,7 @@ public void testModeSettingsCannotBeUsedWhenInDifferentMode() { Settings invalid = Settings.builder().put(settings).put(concreteSetting.getKey(), restrictedSetting.v2()).build(); IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> service.validate(invalid, true)); String expected = "Setting \"" + concreteSetting.getKey() + "\" cannot be used with the configured " + - "\"cluster.remote.cluster_name.mode\" [required=SIMPLE, configured=SNIFF]"; + "\"cluster.remote.cluster_name.mode\" [required=PROXY, configured=SNIFF]"; assertEquals(expected, iae.getMessage()); } } @@ -404,7 +404,7 @@ public void testServerNameAttributes() { ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); - SimpleConnectionStrategy strategy = new SimpleConnectionStrategy(clusterAlias, localService, remoteConnectionManager, + ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, numOfConnections, addresses, true)) { assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterAwareClientTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterAwareClientTests.java index c3a0eefffb67f..1a6eaff9e5a2e 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterAwareClientTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterAwareClientTests.java @@ -65,7 +65,7 @@ public void testSearchShards() throws Exception { knownNodes.add(discoverableTransport.getLocalDiscoNode()); Collections.shuffle(knownNodes, random()); Settings.Builder builder = Settings.builder(); - builder.putList("cluster.remote.cluster1.sniff.seeds", seedTransport.getLocalDiscoNode().getAddress().toString()); + builder.putList("cluster.remote.cluster1.seeds", seedTransport.getLocalDiscoNode().getAddress().toString()); try (MockTransportService service = MockTransportService.createNewService(builder.build(), Version.CURRENT, threadPool, null)) { service.start(); service.acceptIncomingRequests(); @@ -96,7 +96,7 @@ public void testSearchShardsThreadContextHeader() { knownNodes.add(discoverableTransport.getLocalDiscoNode()); Collections.shuffle(knownNodes, random()); Settings.Builder builder = Settings.builder(); - builder.putList("cluster.remote.cluster1.sniff.seeds", seedTransport.getLocalDiscoNode().getAddress().toString()); + builder.putList("cluster.remote.cluster1.seeds", seedTransport.getLocalDiscoNode().getAddress().toString()); try (MockTransportService service = MockTransportService.createNewService(builder.build(), Version.CURRENT, threadPool, null)) { service.start(); service.acceptIncomingRequests(); diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterClientTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterClientTests.java index 76f76b627b569..ed71b7f85c863 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterClientTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterClientTests.java @@ -52,7 +52,7 @@ public void testConnectAndExecuteRequest() throws Exception { Settings localSettings = Settings.builder() .put(RemoteClusterService.ENABLE_REMOTE_CLUSTERS.getKey(), true) - .put("cluster.remote.test.sniff.seeds", + .put("cluster.remote.test.seeds", remoteNode.getAddress().getAddress() + ":" + remoteNode.getAddress().getPort()).build(); try (MockTransportService service = MockTransportService.createNewService(localSettings, Version.CURRENT, threadPool, null)) { service.start(); @@ -81,7 +81,7 @@ public void testEnsureWeReconnect() throws Exception { DiscoveryNode remoteNode = remoteTransport.getLocalDiscoNode(); Settings localSettings = Settings.builder() .put(RemoteClusterService.ENABLE_REMOTE_CLUSTERS.getKey(), true) - .put("cluster.remote.test.sniff.seeds", + .put("cluster.remote.test.seeds", remoteNode.getAddress().getAddress() + ":" + remoteNode.getAddress().getPort()).build(); try (MockTransportService service = MockTransportService.createNewService(localSettings, Version.CURRENT, threadPool, null)) { Semaphore semaphore = new Semaphore(1); diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java index 934cf81a503ea..030174dde5f90 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java @@ -356,7 +356,7 @@ public void testRemoteConnectionInfo() throws IOException { modeInfo1 = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 4, 4); modeInfo2 = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 4, 3); } else { - modeInfo1 = new SimpleConnectionStrategy.SimpleModeInfo(remoteAddresses, 18, 18); + modeInfo1 = new ProxyConnectionStrategy.ProxyModeInfo(remoteAddresses, 18, 18); modeInfo2 = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 18, 17); } @@ -404,7 +404,7 @@ public void testRenderConnectionInfoXContent() throws IOException { if (sniff) { modeInfo = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 3, 2); } else { - modeInfo = new SimpleConnectionStrategy.SimpleModeInfo(remoteAddresses, 18, 16); + modeInfo = new ProxyConnectionStrategy.ProxyModeInfo(remoteAddresses, 18, 16); } RemoteConnectionInfo stats = new RemoteConnectionInfo("test_cluster", modeInfo, TimeValue.timeValueMinutes(30), true); @@ -419,7 +419,7 @@ public void testRenderConnectionInfoXContent() throws IOException { "\"num_nodes_connected\":2,\"max_connections_per_cluster\":3,\"initial_connect_timeout\":\"30m\"," + "\"skip_unavailable\":true}}", Strings.toString(builder)); } else { - assertEquals("{\"test_cluster\":{\"connected\":true,\"mode\":\"simple\",\"addresses\":[\"seed:1\",\"seed:2\"]," + + assertEquals("{\"test_cluster\":{\"connected\":true,\"mode\":\"proxy\",\"addresses\":[\"seed:1\",\"seed:2\"]," + "\"num_sockets_connected\":16,\"max_socket_connections\":18,\"initial_connect_timeout\":\"30m\"," + "\"skip_unavailable\":true}}", Strings.toString(builder)); } @@ -611,30 +611,25 @@ public void sendRequest(long requestId, String action, TransportRequest request, private Settings buildRandomSettings(String clusterAlias, List addresses) { if (randomBoolean()) { - return buildSimpleSettings(clusterAlias, addresses); + return buildProxySettings(clusterAlias, addresses); } else { return buildSniffSettings(clusterAlias, addresses); } } - private static Settings buildSimpleSettings(String clusterAlias, List addresses) { + private static Settings buildProxySettings(String clusterAlias, List addresses) { Settings.Builder builder = Settings.builder(); - builder.put(SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias).getKey(), + builder.put(ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias).getKey(), Strings.collectionToCommaDelimitedString(addresses)); - builder.put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace(clusterAlias).getKey(), "simple"); + builder.put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace(clusterAlias).getKey(), "proxy"); return builder.build(); } private static Settings buildSniffSettings(String clusterAlias, List seedNodes) { Settings.Builder builder = Settings.builder(); builder.put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace(clusterAlias).getKey(), "sniff"); - if (randomBoolean()) { - builder.put(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD.getConcreteSettingForNamespace(clusterAlias).getKey(), - Strings.collectionToCommaDelimitedString(seedNodes)); - } else { - builder.put(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS.getConcreteSettingForNamespace(clusterAlias).getKey(), - Strings.collectionToCommaDelimitedString(seedNodes)); - } + builder.put(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS.getConcreteSettingForNamespace(clusterAlias).getKey(), + Strings.collectionToCommaDelimitedString(seedNodes)); return builder.build(); } } diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java index 0fdc797b8b377..e0e9b69ae29f8 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java @@ -84,27 +84,26 @@ public void testSettingsAreRegistered() { assertTrue(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS.contains(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE)); assertTrue(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS.contains(SniffConnectionStrategy.REMOTE_CONNECTIONS_PER_CLUSTER)); assertTrue(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS.contains(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS)); - assertTrue(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS.contains(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD)); assertTrue(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS.contains(SniffConnectionStrategy.REMOTE_NODE_CONNECTIONS)); - assertTrue(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS.contains(SimpleConnectionStrategy.REMOTE_CLUSTER_ADDRESSES)); - assertTrue(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS.contains(SimpleConnectionStrategy.REMOTE_SOCKET_CONNECTIONS)); + assertTrue(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS.contains(ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES)); + assertTrue(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS.contains(ProxyConnectionStrategy.REMOTE_SOCKET_CONNECTIONS)); } public void testRemoteClusterSeedSetting() { // simple validation Settings settings = Settings.builder() - .put("cluster.remote.foo.sniff.seeds", "192.168.0.1:8080") - .put("cluster.remote.bar.sniff.seed", "[::1]:9090").build(); + .put("cluster.remote.foo.seeds", "192.168.0.1:8080") + .put("cluster.remote.bar.seeds", "[::1]:9090").build(); SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS.getAllConcreteSettings(settings).forEach(setting -> setting.get(settings)); Settings brokenSettings = Settings.builder() - .put("cluster.remote.foo.sniff.seeds", "192.168.0.1").build(); + .put("cluster.remote.foo.seeds", "192.168.0.1").build(); expectThrows(IllegalArgumentException.class, () -> SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS .getAllConcreteSettings(brokenSettings).forEach(setting -> setting.get(brokenSettings))); Settings brokenPortSettings = Settings.builder() - .put("cluster.remote.foo.sniff.seeds", "192.168.0.1:123456789123456789").build(); + .put("cluster.remote.foo.seeds", "192.168.0.1:123456789123456789").build(); Exception e = expectThrows( IllegalArgumentException.class, () -> SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS.getAllConcreteSettings(brokenSettings) @@ -128,8 +127,8 @@ public void testGroupClusterIndices() throws IOException { transportService.start(); transportService.acceptIncomingRequests(); Settings.Builder builder = Settings.builder(); - builder.putList("cluster.remote.cluster_1.sniff.seeds", cluster1Seed.getAddress().toString()); - builder.putList("cluster.remote.cluster_2.sniff.seeds", cluster2Seed.getAddress().toString()); + builder.putList("cluster.remote.cluster_1.seeds", cluster1Seed.getAddress().toString()); + builder.putList("cluster.remote.cluster_2.seeds", cluster2Seed.getAddress().toString()); try (RemoteClusterService service = new RemoteClusterService(builder.build(), transportService)) { assertFalse(service.isCrossClusterSearchEnabled()); service.initializeRemoteClusters(); @@ -354,7 +353,7 @@ public void testChangeSettings() throws Exception { transportService.start(); transportService.acceptIncomingRequests(); Settings.Builder builder = Settings.builder(); - builder.putList("cluster.remote.cluster_1.sniff.seeds", cluster1Seed.getAddress().toString()); + builder.putList("cluster.remote.cluster_1.seeds", cluster1Seed.getAddress().toString()); try (RemoteClusterService service = new RemoteClusterService(builder.build(), transportService)) { service.initializeRemoteClusters(); RemoteClusterConnection remoteClusterConnection = service.getRemoteClusterConnection("cluster_1"); @@ -363,7 +362,7 @@ public void testChangeSettings() throws Exception { settingsChange.put("cluster.remote.cluster_1.transport.ping_schedule", pingSchedule); boolean compressionEnabled = true; settingsChange.put("cluster.remote.cluster_1.transport.compress", compressionEnabled); - settingsChange.putList("cluster.remote.cluster_1.sniff.seeds", cluster1Seed.getAddress().toString()); + settingsChange.putList("cluster.remote.cluster_1.seeds", cluster1Seed.getAddress().toString()); service.validateAndUpdateRemoteCluster("cluster_1", settingsChange.build()); assertBusy(remoteClusterConnection::isClosed); @@ -408,9 +407,9 @@ public void testRemoteNodeAttribute() throws IOException, InterruptedException { transportService.acceptIncomingRequests(); final Settings.Builder builder = Settings.builder(); builder.putList( - "cluster.remote.cluster_1.sniff.seeds", c1N1Node.getAddress().toString()); + "cluster.remote.cluster_1.seed", c1N1Node.getAddress().toString()); builder.putList( - "cluster.remote.cluster_2.sniff.seeds", c2N1Node.getAddress().toString()); + "cluster.remote.cluster_2.seed", c2N1Node.getAddress().toString()); try (RemoteClusterService service = new RemoteClusterService(settings, transportService)) { assertFalse(service.isCrossClusterSearchEnabled()); @@ -475,8 +474,8 @@ public void testRemoteNodeRoles() throws IOException, InterruptedException { transportService.start(); transportService.acceptIncomingRequests(); final Settings.Builder builder = Settings.builder(); - builder.putList("cluster.remote.cluster_1.sniff.seeds", c1N1Node.getAddress().toString()); - builder.putList("cluster.remote.cluster_2.sniff.seeds", c2N1Node.getAddress().toString()); + builder.putList("cluster.remote.cluster_1.seed", c1N1Node.getAddress().toString()); + builder.putList("cluster.remote.cluster_2.seed", c2N1Node.getAddress().toString()); try (RemoteClusterService service = new RemoteClusterService(settings, transportService)) { assertFalse(service.isCrossClusterSearchEnabled()); service.initializeRemoteClusters(); @@ -546,9 +545,9 @@ public void testCollectNodes() throws InterruptedException, IOException { transportService.acceptIncomingRequests(); final Settings.Builder builder = Settings.builder(); builder.putList( - "cluster.remote.cluster_1.sniff.seeds", c1N1Node.getAddress().toString()); + "cluster.remote.cluster_1.seed", c1N1Node.getAddress().toString()); builder.putList( - "cluster.remote.cluster_2.sniff.seeds", c2N1Node.getAddress().toString()); + "cluster.remote.cluster_2.seed", c2N1Node.getAddress().toString()); try (RemoteClusterService service = new RemoteClusterService(settings, transportService)) { assertFalse(service.isCrossClusterSearchEnabled()); @@ -678,12 +677,12 @@ public void testRemoteClusterSkipIfDisconnectedSetting() { } AbstractScopedSettings service = new ClusterSettings(Settings.EMPTY, - new HashSet<>(Arrays.asList(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS, SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD, - RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE))); + new HashSet<>(Arrays.asList(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS, + RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE))); { Settings settings = Settings.builder().put("cluster.remote.foo.skip_unavailable", randomBoolean()).build(); IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> service.validate(settings, true)); - assertEquals("missing required setting [cluster.remote.foo.sniff.seeds] for setting [cluster.remote.foo.skip_unavailable]", + assertEquals("missing required setting [cluster.remote.foo.seeds] for setting [cluster.remote.foo.skip_unavailable]", iae.getMessage()); } { @@ -691,11 +690,11 @@ public void testRemoteClusterSkipIfDisconnectedSetting() { String seed = remoteSeedTransport.getLocalDiscoNode().getAddress().toString(); service.validate(Settings.builder().put("cluster.remote.foo.skip_unavailable", randomBoolean()) .put("cluster.remote.foo.seeds", seed).build(), true); - service.validate(Settings.builder().put("cluster.remote.foo.sniff.seeds", seed).build(), true); + service.validate(Settings.builder().put("cluster.remote.foo.seeds", seed).build(), true); AbstractScopedSettings service2 = new ClusterSettings(Settings.builder().put("cluster.remote.foo.seeds", seed).build(), - new HashSet<>(Arrays.asList(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD, - RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE))); + new HashSet<>(Arrays.asList(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS, + RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE))); service2.validate(Settings.builder().put("cluster.remote.foo.skip_unavailable", randomBoolean()).build(), false); } } @@ -718,7 +717,7 @@ public void testReconnectWhenStrategySettingsUpdated() throws Exception { transportService.acceptIncomingRequests(); final Settings.Builder builder = Settings.builder(); - builder.putList("cluster.remote.cluster_test.sniff.seeds", Collections.singletonList(node0.getAddress().toString())); + builder.putList("cluster.remote.cluster_test.seeds", Collections.singletonList(node0.getAddress().toString())); try (RemoteClusterService service = new RemoteClusterService(builder.build(), transportService)) { assertFalse(service.isCrossClusterSearchEnabled()); service.initializeRemoteClusters(); @@ -814,13 +813,8 @@ public void testSkipUnavailable() { private static Settings createSettings(String clusterAlias, List seeds) { Settings.Builder builder = Settings.builder(); - if (randomBoolean()) { - builder.put(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD.getConcreteSettingForNamespace(clusterAlias).getKey(), - Strings.collectionToCommaDelimitedString(seeds)); - } else { - builder.put(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS.getConcreteSettingForNamespace(clusterAlias).getKey(), - Strings.collectionToCommaDelimitedString(seeds)); - } + builder.put(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS.getConcreteSettingForNamespace(clusterAlias).getKey(), + Strings.collectionToCommaDelimitedString(seeds)); return builder.build(); } } diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterSettingsTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterSettingsTests.java index af855314278f7..0f2749ffacf6c 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterSettingsTests.java @@ -26,9 +26,9 @@ import java.util.concurrent.TimeUnit; import static org.elasticsearch.transport.SniffConnectionStrategy.REMOTE_CLUSTERS_PROXY; -import static org.elasticsearch.transport.SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD; import static org.elasticsearch.transport.RemoteClusterService.ENABLE_REMOTE_CLUSTERS; import static org.elasticsearch.transport.RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE; +import static org.elasticsearch.transport.SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS; import static org.elasticsearch.transport.SniffConnectionStrategy.REMOTE_CONNECTIONS_PER_CLUSTER; import static org.elasticsearch.transport.RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING; import static org.elasticsearch.transport.RemoteClusterService.REMOTE_NODE_ATTRIBUTE; @@ -60,7 +60,7 @@ public void testSkipUnavailableDefault() { public void testSeedsDefault() { final String alias = randomAlphaOfLength(8); - assertThat(REMOTE_CLUSTER_SEEDS_OLD.getConcreteSettingForNamespace(alias).get(Settings.EMPTY), emptyCollectionOf(String.class)); + assertThat(REMOTE_CLUSTER_SEEDS.getConcreteSettingForNamespace(alias).get(Settings.EMPTY), emptyCollectionOf(String.class)); } public void testProxyDefault() { diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteConnectionStrategyTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteConnectionStrategyTests.java index 2c6fc691ec5bd..814a3bd1913b1 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteConnectionStrategyTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteConnectionStrategyTests.java @@ -33,7 +33,7 @@ public void testStrategyChangeMeansThatStrategyMustBeRebuilt() { ConnectionManager connectionManager = new ConnectionManager(Settings.EMPTY, mock(Transport.class)); RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager("cluster-alias", connectionManager); FakeConnectionStrategy first = new FakeConnectionStrategy("cluster-alias", mock(TransportService.class), remoteConnectionManager, - RemoteConnectionStrategy.ConnectionStrategy.SIMPLE); + RemoteConnectionStrategy.ConnectionStrategy.PROXY); Settings newSettings = Settings.builder() .put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace("cluster-alias").getKey(), "sniff") .build(); @@ -44,9 +44,9 @@ public void testSameStrategyChangeMeansThatStrategyDoesNotNeedToBeRebuilt() { ConnectionManager connectionManager = new ConnectionManager(Settings.EMPTY, mock(Transport.class)); RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager("cluster-alias", connectionManager); FakeConnectionStrategy first = new FakeConnectionStrategy("cluster-alias", mock(TransportService.class), remoteConnectionManager, - RemoteConnectionStrategy.ConnectionStrategy.SIMPLE); + RemoteConnectionStrategy.ConnectionStrategy.PROXY); Settings newSettings = Settings.builder() - .put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace("cluster-alias").getKey(), "simple") + .put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace("cluster-alias").getKey(), "proxy") .build(); assertFalse(first.shouldRebuildConnection(newSettings)); } @@ -57,10 +57,10 @@ public void testChangeInConnectionProfileMeansTheStrategyMustBeRebuilt() { assertEquals(false, connectionManager.getConnectionProfile().getCompressionEnabled()); RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager("cluster-alias", connectionManager); FakeConnectionStrategy first = new FakeConnectionStrategy("cluster-alias", mock(TransportService.class), remoteConnectionManager, - RemoteConnectionStrategy.ConnectionStrategy.SIMPLE); + RemoteConnectionStrategy.ConnectionStrategy.PROXY); Settings.Builder newBuilder = Settings.builder(); - newBuilder.put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace("cluster-alias").getKey(), "simple"); + newBuilder.put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace("cluster-alias").getKey(), "proxy"); if (randomBoolean()) { newBuilder.put(RemoteClusterService.REMOTE_CLUSTER_PING_SCHEDULE.getConcreteSettingForNamespace("cluster-alias").getKey(), TimeValue.timeValueSeconds(5)); @@ -75,10 +75,10 @@ public void testCorrectChannelNumber() { for (RemoteConnectionStrategy.ConnectionStrategy strategy : RemoteConnectionStrategy.ConnectionStrategy.values()) { String settingKey = RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace(clusterAlias).getKey(); - Settings simpleSettings = Settings.builder().put(settingKey, strategy.name()).build(); - ConnectionProfile simpleProfile = RemoteConnectionStrategy.buildConnectionProfile(clusterAlias, simpleSettings); + Settings proxySettings = Settings.builder().put(settingKey, strategy.name()).build(); + ConnectionProfile proxyProfile = RemoteConnectionStrategy.buildConnectionProfile(clusterAlias, proxySettings); assertEquals("Incorrect number of channels for " + strategy.name(), - strategy.getNumberOfChannels(), simpleProfile.getNumConnections()); + strategy.getNumberOfChannels(), proxyProfile.getNumConnections()); } } diff --git a/server/src/test/java/org/elasticsearch/transport/SniffConnectionStrategyTests.java b/server/src/test/java/org/elasticsearch/transport/SniffConnectionStrategyTests.java index 30f30723c19a8..9302e61d13710 100644 --- a/server/src/test/java/org/elasticsearch/transport/SniffConnectionStrategyTests.java +++ b/server/src/test/java/org/elasticsearch/transport/SniffConnectionStrategyTests.java @@ -652,14 +652,13 @@ public void testGetNodePredicatesCombination() { public void testModeSettingsCannotBeUsedWhenInDifferentMode() { List, String>> restrictedSettings = Arrays.asList( new Tuple<>(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS, "192.168.0.1:8080"), - new Tuple<>(SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD, "192.168.0.1:8080"), new Tuple<>(SniffConnectionStrategy.REMOTE_NODE_CONNECTIONS, "2")); - RemoteConnectionStrategy.ConnectionStrategy simple = RemoteConnectionStrategy.ConnectionStrategy.SIMPLE; + RemoteConnectionStrategy.ConnectionStrategy proxy = RemoteConnectionStrategy.ConnectionStrategy.PROXY; String clusterName = "cluster_name"; Settings settings = Settings.builder() - .put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace(clusterName).getKey(), simple.name()) + .put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace(clusterName).getKey(), proxy.name()) .build(); Set> clusterSettings = new HashSet<>(); @@ -675,7 +674,7 @@ public void testModeSettingsCannotBeUsedWhenInDifferentMode() { Settings invalid = Settings.builder().put(settings).put(concreteSetting.getKey(), restrictedSetting.v2()).build(); IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> service.validate(invalid, true)); String expected = "Setting \"" + concreteSetting.getKey() + "\" cannot be used with the configured " + - "\"cluster.remote.cluster_name.mode\" [required=SNIFF, configured=SIMPLE]"; + "\"cluster.remote.cluster_name.mode\" [required=SNIFF, configured=PROXY]"; assertEquals(expected, iae.getMessage()); } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IndexFollowingIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IndexFollowingIT.java index 7bede18aea08d..e25c0be5731ae 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IndexFollowingIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IndexFollowingIT.java @@ -1389,7 +1389,7 @@ public void testUpdateRemoteConfigsDuringFollowing() throws Exception { ClusterUpdateSettingsRequest settingsRequest = new ClusterUpdateSettingsRequest(); String address = getLeaderCluster().getDataNodeInstance(TransportService.class).boundAddress().publishAddress().toString(); Setting compress = RemoteClusterService.REMOTE_CLUSTER_COMPRESS.getConcreteSettingForNamespace("leader_cluster"); - Setting> seeds = SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS_OLD.getConcreteSettingForNamespace("leader_cluster"); + Setting> seeds = SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS.getConcreteSettingForNamespace("leader_cluster"); settingsRequest.persistentSettings(Settings.builder().put(compress.getKey(), compress.getDefault(Settings.EMPTY)) .put(seeds.getKey(), address)); assertAcked(followerClient().admin().cluster().updateSettings(settingsRequest).actionGet()); diff --git a/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/multi_cluster/70_connection_mode_configuration.yml b/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/multi_cluster/70_connection_mode_configuration.yml index ed639b3655ed5..5606a08cd261e 100644 --- a/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/multi_cluster/70_connection_mode_configuration.yml +++ b/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/multi_cluster/70_connection_mode_configuration.yml @@ -1,5 +1,5 @@ --- -"Add transient remote cluster in simple mode with invalid sniff settings": +"Add transient remote cluster in proxy mode with invalid sniff settings": - do: cluster.get_settings: include_defaults: true @@ -12,14 +12,14 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.sniff.node_connections: "5" - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.node_connections: "5" + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.sniff.node_connections\" cannot be - used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=SIMPLE]" } + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.node_connections\" cannot be + used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=PROXY]" } - do: catch: bad_request @@ -27,17 +27,17 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.seeds: $remote_ip + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.sniff.seeds\" cannot be - used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=SIMPLE]" } + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.seeds\" cannot be + used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=PROXY]" } --- -"Add transient remote cluster in sniff mode with invalid simple settings": +"Add transient remote cluster in sniff mode with invalid proxy settings": - do: cluster.get_settings: include_defaults: true @@ -50,13 +50,13 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.simple.socket_connections: "20" - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip + cluster.remote.test_remote_cluster.proxy_socket_connections: "20" + cluster.remote.test_remote_cluster.seeds: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.simple.socket_connections\" cannot be - used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SIMPLE, configured=SNIFF]" } + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.proxy_socket_connections\" cannot be + used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=PROXY, configured=SNIFF]" } - do: catch: bad_request @@ -64,16 +64,16 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.seeds: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.simple.addresses\" cannot be - used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SIMPLE, configured=SNIFF]" } + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.proxy_addresses\" cannot be + used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=PROXY, configured=SNIFF]" } --- -"Add transient remote cluster using simple connection mode using valid settings": +"Add transient remote cluster using proxy connection mode using valid settings": - do: cluster.get_settings: include_defaults: true @@ -85,13 +85,13 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.simple.socket_connections: "3" - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.proxy_socket_connections: "3" + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "simple"} - - match: {transient.cluster\.remote\.test_remote_cluster\.simple\.socket_connections: "3"} - - match: {transient.cluster\.remote\.test_remote_cluster\.simple\.addresses: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "proxy"} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_socket_connections: "3"} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_addresses: $remote_ip} - do: search: @@ -120,12 +120,12 @@ body: transient: cluster.remote.test_remote_cluster.mode: "sniff" - cluster.remote.test_remote_cluster.sniff.node_connections: "3" - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip + cluster.remote.test_remote_cluster.node_connections: "3" + cluster.remote.test_remote_cluster.seeds: $remote_ip - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "sniff"} - - match: {transient.cluster\.remote\.test_remote_cluster\.sniff\.node_connections: "3"} - - match: {transient.cluster\.remote\.test_remote_cluster\.sniff\.seeds: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.node_connections: "3"} + - match: {transient.cluster\.remote\.test_remote_cluster\.seeds: $remote_ip} - do: search: @@ -154,10 +154,10 @@ body: transient: cluster.remote.test_remote_cluster.mode: "sniff" - cluster.remote.test_remote_cluster.sniff.seeds: $remote_ip + cluster.remote.test_remote_cluster.seeds: $remote_ip - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "sniff"} - - match: {transient.cluster\.remote\.test_remote_cluster\.sniff\.seeds: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.seeds: $remote_ip} - do: search: @@ -178,25 +178,25 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.sniff.seeds\" cannot be - used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=SIMPLE]" } + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.seeds\" cannot be + used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=SNIFF, configured=PROXY]" } - do: cluster.put_settings: flat_settings: true body: transient: - cluster.remote.test_remote_cluster.mode: "simple" - cluster.remote.test_remote_cluster.sniff.seeds: null - cluster.remote.test_remote_cluster.simple.addresses: $remote_ip + cluster.remote.test_remote_cluster.mode: "proxy" + cluster.remote.test_remote_cluster.seeds: null + cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip - - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "simple"} - - match: {transient.cluster\.remote\.test_remote_cluster\.simple\.addresses: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "proxy"} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_addresses: $remote_ip} - do: search: From a77d5865e413700553421afc395509b7875707cf Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Thu, 19 Dec 2019 17:50:59 +0100 Subject: [PATCH 275/686] Remove snapshots left by previous tests failures (#50380) When a third party test failed, it potentially left some snapshots in the repository. In case of tests running against an external service like Azure, the remaining snapshots can fail the future test executions are they are not supposed to exist. Similarly to what has been done for S3 and GCS, this commit cleans up remaining snapshots before the test execution. Closes #50304 --- .../test/repository_azure/10_repository.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plugins/repository-azure/qa/microsoft-azure-storage/src/test/resources/rest-api-spec/test/repository_azure/10_repository.yml b/plugins/repository-azure/qa/microsoft-azure-storage/src/test/resources/rest-api-spec/test/repository_azure/10_repository.yml index 088c4d28ef718..187908c0eecd1 100644 --- a/plugins/repository-azure/qa/microsoft-azure-storage/src/test/resources/rest-api-spec/test/repository_azure/10_repository.yml +++ b/plugins/repository-azure/qa/microsoft-azure-storage/src/test/resources/rest-api-spec/test/repository_azure/10_repository.yml @@ -13,6 +13,19 @@ setup: client: "integration_test" base_path: ${base_path} + # Remove the snapshots, if a previous test failed to delete them. This is + # useful for third party tests that runs the test against a real external service. + - do: + snapshot.delete: + repository: repository + snapshot: snapshot-one + ignore: 404 + - do: + snapshot.delete: + repository: repository + snapshot: snapshot-two + ignore: 404 + --- "Snapshot/Restore with repository-azure": - skip: From 6d4582e8b126468e2d634a25c4358bb069586e98 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Thu, 19 Dec 2019 17:53:09 +0100 Subject: [PATCH 276/686] [DOCS] Convert main README to asciidoc (#50303) Converts the main README file to asciidoc. Also includes the following changes: * Mentioning Slack instead of IRC * Removed mentioning of native java API, HTTP should be used * Removed java as a installed requirement --- README.textile => README.asciidoc | 103 ++++++++++++++---------------- 1 file changed, 49 insertions(+), 54 deletions(-) rename README.textile => README.asciidoc (70%) diff --git a/README.textile b/README.asciidoc similarity index 70% rename from README.textile rename to README.asciidoc index 2f7b0472c238e..962608f5b45ba 100644 --- a/README.textile +++ b/README.asciidoc @@ -1,8 +1,8 @@ -h1. Elasticsearch += Elasticsearch -h2. A Distributed RESTful Search Engine +== A Distributed RESTful Search Engine -h3. "https://www.elastic.co/products/elasticsearch":https://www.elastic.co/products/elasticsearch +=== https://www.elastic.co/products/elasticsearch[https://www.elastic.co/products/elasticsearch] Elasticsearch is a distributed RESTful search engine built for the cloud. Features include: @@ -15,39 +15,34 @@ Elasticsearch is a distributed RESTful search engine built for the cloud. Featur ** Index level configuration (number of shards, index storage, ...). * Various set of APIs ** HTTP RESTful API -** Native Java API. ** All APIs perform automatic node operation rerouting. * Document oriented ** No need for upfront schema definition. ** Schema can be defined for customization of the indexing process. * Reliable, Asynchronous Write Behind for long term persistency. * (Near) Real Time Search. -* Built on top of Lucene +* Built on top of Apache Lucene ** Each shard is a fully functional Lucene index ** All the power of Lucene easily exposed through simple configuration / plugins. * Per operation consistency ** Single document level operations are atomic, consistent, isolated and durable. -h2. Getting Started +== Getting Started First of all, DON'T PANIC. It will take 5 minutes to get the gist of what Elasticsearch is all about. -h3. Requirements +=== Installation -You need to have a recent version of Java installed. See the "Setup":http://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html#jvm-version page for more information. - -h3. Installation - -* "Download":https://www.elastic.co/downloads/elasticsearch and unzip the Elasticsearch official distribution. -* Run @bin/elasticsearch@ on unix, or @bin\elasticsearch.bat@ on windows. -* Run @curl -X GET http://localhost:9200/@. +* https://www.elastic.co/downloads/elasticsearch[Download] and unpack the Elasticsearch official distribution. +* Run `bin/elasticsearch` on Linux or macOS. Run `bin\elasticsearch.bat` on Windows. +* Run `curl -X GET http://localhost:9200/`. * Start more servers ... -h3. Indexing +=== Indexing -Let's try and index some twitter like information. First, let's index some tweets (the @twitter@ index will be created automatically): +Let's try and index some twitter like information. First, let's index some tweets (the `twitter` index will be created automatically): -
    +----
     curl -XPUT 'http://localhost:9200/twitter/_doc/1?pretty' -H 'Content-Type: application/json' -d '
     {
         "user": "kimchy",
    @@ -68,50 +63,50 @@ curl -XPUT 'http://localhost:9200/twitter/_doc/3?pretty' -H 'Content-Type: appli
         "post_date": "2010-01-15T01:46:38",
         "message": "Building the site, should be kewl"
     }'
    -
    +---- Now, let's see if the information was added by GETting it: -
    +----
     curl -XGET 'http://localhost:9200/twitter/_doc/1?pretty=true'
     curl -XGET 'http://localhost:9200/twitter/_doc/2?pretty=true'
     curl -XGET 'http://localhost:9200/twitter/_doc/3?pretty=true'
    -
    +---- -h3. Searching +=== Searching Mmm search..., shouldn't it be elastic? -Let's find all the tweets that @kimchy@ posted: +Let's find all the tweets that `kimchy` posted: -
    +----
     curl -XGET 'http://localhost:9200/twitter/_search?q=user:kimchy&pretty=true'
    -
    +---- We can also use the JSON query language Elasticsearch provides instead of a query string: -
    +----
     curl -XGET 'http://localhost:9200/twitter/_search?pretty=true' -H 'Content-Type: application/json' -d '
     {
         "query" : {
             "match" : { "user": "kimchy" }
         }
     }'
    -
    +---- -Just for kicks, let's get all the documents stored (we should see the tweet from @elastic@ as well): +Just for kicks, let's get all the documents stored (we should see the tweet from `elastic` as well): -
    +----
     curl -XGET 'http://localhost:9200/twitter/_search?pretty=true' -H 'Content-Type: application/json' -d '
     {
         "query" : {
             "match_all" : {}
         }
     }'
    -
    +---- -We can also do range search (the @post_date@ was automatically identified as date) +We can also do range search (the `post_date` was automatically identified as date) -
    +----
     curl -XGET 'http://localhost:9200/twitter/_search?pretty=true' -H 'Content-Type: application/json' -d '
     {
         "query" : {
    @@ -120,19 +115,19 @@ curl -XGET 'http://localhost:9200/twitter/_search?pretty=true' -H 'Content-Type:
             }
         }
     }'
    -
    +---- There are many more options to perform search, after all, it's a search product no? All the familiar Lucene queries are available through the JSON query language, or through the query parser. -h3. Multi Tenant and Indices +=== Multi Tenant and Indices Man, that twitter index might get big (in this case, index size == valuation). Let's see if we can structure our twitter system a bit differently in order to support such large amounts of data. -Elasticsearch supports multiple indices. In the previous example we used an index called @twitter@ that stored tweets for every user. +Elasticsearch supports multiple indices. In the previous example we used an index called `twitter` that stored tweets for every user. Another way to define our simple twitter system is to have a different index per user (note, though that each index has an overhead). Here is the indexing curl's in this case: -
    +----
     curl -XPUT 'http://localhost:9200/kimchy/_doc/1?pretty' -H 'Content-Type: application/json' -d '
     {
         "user": "kimchy",
    @@ -146,13 +141,13 @@ curl -XPUT 'http://localhost:9200/kimchy/_doc/2?pretty' -H 'Content-Type: applic
         "post_date": "2009-11-15T14:12:12",
         "message": "Another tweet, will it be indexed?"
     }'
    -
    +---- -The above will index information into the @kimchy@ index. Each user will get their own special index. +The above will index information into the `kimchy` index. Each user will get their own special index. Complete control on the index level is allowed. As an example, in the above case, we might want to change from the default 1 shard with 1 replica per index, to 2 shards with 1 replica per index (because this user tweets a lot). Here is how this can be done (the configuration can be in yaml as well): -
    +----
     curl -XPUT http://localhost:9200/another_user?pretty -H 'Content-Type: application/json' -d '
     {
         "settings" : {
    @@ -160,34 +155,34 @@ curl -XPUT http://localhost:9200/another_user?pretty -H 'Content-Type: applicati
             "index.number_of_replicas" : 1
         }
     }'
    -
    +---- Search (and similar operations) are multi index aware. This means that we can easily search on more than one index (twitter user), for example: -
    +----
     curl -XGET 'http://localhost:9200/kimchy,another_user/_search?pretty=true' -H 'Content-Type: application/json' -d '
     {
         "query" : {
             "match_all" : {}
         }
     }'
    -
    +---- Or on all the indices: -
    +----
     curl -XGET 'http://localhost:9200/_search?pretty=true' -H 'Content-Type: application/json' -d '
     {
         "query" : {
             "match_all" : {}
         }
     }'
    -
    +---- -{One liner teaser}: And the cool part about that? You can easily search on multiple twitter users (indices), with different boost levels per user (index), making social search so much simpler (results from my friends rank higher than results from friends of my friends). +And the cool part about that? You can easily search on multiple twitter users (indices), with different boost levels per user (index), making social search so much simpler (results from my friends rank higher than results from friends of my friends). -h3. Distributed, Highly Available +=== Distributed, Highly Available Let's face it, things will fail.... @@ -195,20 +190,20 @@ Elasticsearch is a highly available and distributed search engine. Each index is In order to play with the distributed nature of Elasticsearch, simply bring more nodes up and shut down nodes. The system will continue to serve requests (make sure you use the correct http port) with the latest data indexed. -h3. Where to go from here? +=== Where to go from here? -We have just covered a very small portion of what Elasticsearch is all about. For more information, please refer to the "elastic.co":http://www.elastic.co/products/elasticsearch website. General questions can be asked on the "Elastic Discourse forum":https://discuss.elastic.co or on IRC on Freenode at "#elasticsearch":https://webchat.freenode.net/#elasticsearch. The Elasticsearch GitHub repository is reserved for bug reports and feature requests only. +We have just covered a very small portion of what Elasticsearch is all about. For more information, please refer to the http://www.elastic.co/products/elasticsearch[elastic.co] website. General questions can be asked on the https://discuss.elastic.co[Elastic Forum] or https://ela.st/slack[on Slack]. The Elasticsearch GitHub repository is reserved for bug reports and feature requests only. -h3. Building from Source +=== Building from Source -Elasticsearch uses "Gradle":https://gradle.org for its build system. +Elasticsearch uses https://gradle.org[Gradle] for its build system. -In order to create a distribution, simply run the @./gradlew assemble@ command in the cloned directory. +In order to create a distribution, simply run the `./gradlew assemble` command in the cloned directory. -The distribution for each project will be created under the @build/distributions@ directory in that project. +The distribution for each project will be created under the `build/distributions` directory in that project. -See the "TESTING":TESTING.asciidoc file for more information about running the Elasticsearch test suite. +See the xref:TESTING.asciidoc[TESTING] for more information about running the Elasticsearch test suite. -h3. Upgrading from older Elasticsearch versions +=== Upgrading from older Elasticsearch versions -In order to ensure a smooth upgrade process from earlier versions of Elasticsearch, please see our "upgrade documentation":https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html for more details on the upgrade process. +In order to ensure a smooth upgrade process from earlier versions of Elasticsearch, please see our https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html[upgrade documentation] for more details on the upgrade process. From a54d6cb59c1fde97e41d49e64cedcb93173bfbe0 Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Thu, 19 Dec 2019 10:14:28 -0700 Subject: [PATCH 277/686] Scripting: ScriptFactory not required by compile (#50344) Avoid backwards incompatible changes for 8.x and 7.6 by removing type restriction on compile and Factory. Factories may optionally implement ScriptFactory. If so, then they can indicate determinism and thus cacheability. Relates: #49466 --- .../common/AnalysisPredicateScript.java | 3 +- .../PredicateTokenScriptFilterTests.java | 3 +- .../ScriptedConditionTokenFilterTests.java | 3 +- .../expression/ExpressionScriptEngine.java | 63 ++++++++++++++++--- .../script/mustache/MustacheScriptEngine.java | 6 +- .../painless/PainlessScriptEngine.java | 7 +-- .../action/PainlessExecuteAction.java | 3 +- .../painless/BaseClassTests.java | 43 +++++++------ .../painless/BasicStatementTests.java | 3 +- .../elasticsearch/painless/BindingsTests.java | 3 +- .../elasticsearch/painless/FactoryTests.java | 44 ++++++++++--- .../expertscript/ExpertScriptPlugin.java | 5 +- .../index/query/QueryShardContext.java | 6 +- .../BucketAggregationSelectorScript.java | 2 +- .../script/IngestConditionalScript.java | 2 +- .../elasticsearch/script/IngestScript.java | 2 +- .../elasticsearch/script/ScriptContext.java | 2 +- .../elasticsearch/script/ScriptEngine.java | 2 +- .../elasticsearch/script/ScriptService.java | 2 +- .../elasticsearch/script/TemplateScript.java | 2 +- .../elasticsearch/script/UpdateScript.java | 2 +- .../query/IntervalQueryBuilderTests.java | 3 +- .../script/ScriptContextTests.java | 12 ++-- .../script/ScriptLanguagesInfoTests.java | 2 +- .../metrics/ScriptedMetricIT.java | 1 - .../functionscore/ExplainableScriptIT.java | 3 +- .../search/suggest/SuggestSearchIT.java | 3 +- .../ingest/TestTemplateService.java | 3 +- .../script/MockScriptEngine.java | 19 +++--- .../script/MockMustacheScriptEngine.java | 2 +- .../test/MockPainlessScriptEngine.java | 3 +- .../condition/WatcherConditionScript.java | 3 +- .../script/WatcherTransformScript.java | 3 +- 33 files changed, 160 insertions(+), 105 deletions(-) diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java index f14bd9ded9cc1..5d8c491efc585 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java @@ -27,7 +27,6 @@ import org.apache.lucene.analysis.tokenattributes.TypeAttribute; import org.apache.lucene.util.AttributeSource; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; /** * A predicate based on the current token in a TokenStream @@ -108,7 +107,7 @@ public boolean isKeyword() { */ public abstract boolean execute(Token token); - public interface Factory extends ScriptFactory { + public interface Factory { AnalysisPredicateScript newInstance(); } diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java index 9e61a59237348..84ba5e5d3373c 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java @@ -30,7 +30,6 @@ import org.elasticsearch.indices.analysis.AnalysisModule; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTokenStreamTestCase; import org.elasticsearch.test.IndexSettingsModule; @@ -64,7 +63,7 @@ public boolean execute(Token token) { @SuppressWarnings("unchecked") ScriptService scriptService = new ScriptService(indexSettings, Collections.emptyMap(), Collections.emptyMap()){ @Override - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { assertEquals(context, AnalysisPredicateScript.CONTEXT); assertEquals(new Script("my_script"), script); return (FactoryType) factory; diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterTests.java index d2bbc780c8747..58226ac169bc3 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterTests.java @@ -30,7 +30,6 @@ import org.elasticsearch.indices.analysis.AnalysisModule; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTokenStreamTestCase; import org.elasticsearch.test.IndexSettingsModule; @@ -64,7 +63,7 @@ public boolean execute(Token token) { @SuppressWarnings("unchecked") ScriptService scriptService = new ScriptService(indexSettings, Collections.emptyMap(), Collections.emptyMap()){ @Override - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { assertEquals(context, AnalysisPredicateScript.CONTEXT); assertEquals(new Script("token.getPosition() > 1"), script); return (FactoryType) factory; diff --git a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java index ead6d733b10de..b99730be826d5 100644 --- a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java +++ b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java @@ -44,7 +44,6 @@ import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; import org.elasticsearch.script.ScriptException; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.TermsSetQueryScript; import org.elasticsearch.search.lookup.SearchLookup; @@ -85,22 +84,72 @@ public boolean execute() { }, FilterScript.CONTEXT, - (Expression expr) -> (FilterScript.Factory) (p, lookup) -> newFilterScript(expr, lookup, p), + (Expression expr) -> new FilterScript.Factory() { + @Override + public boolean isResultDeterministic() { + return true; + } + + @Override + public FilterScript.LeafFactory newFactory(Map params, SearchLookup lookup) { + return newFilterScript(expr, lookup, params); + } + }, ScoreScript.CONTEXT, - (Expression expr) -> (ScoreScript.Factory) (p, lookup) -> newScoreScript(expr, lookup, p), + (Expression expr) -> new ScoreScript.Factory() { + @Override + public ScoreScript.LeafFactory newFactory(Map params, SearchLookup lookup) { + return newScoreScript(expr, lookup, params); + } + + @Override + public boolean isResultDeterministic() { + return true; + } + }, TermsSetQueryScript.CONTEXT, (Expression expr) -> (TermsSetQueryScript.Factory) (p, lookup) -> newTermsSetQueryScript(expr, lookup, p), AggregationScript.CONTEXT, - (Expression expr) -> (AggregationScript.Factory) (p, lookup) -> newAggregationScript(expr, lookup, p), + (Expression expr) -> new AggregationScript.Factory() { + @Override + public AggregationScript.LeafFactory newFactory(Map params, SearchLookup lookup) { + return newAggregationScript(expr, lookup, params); + } + + @Override + public boolean isResultDeterministic() { + return true; + } + }, NumberSortScript.CONTEXT, - (Expression expr) -> (NumberSortScript.Factory) (p, lookup) -> newSortScript(expr, lookup, p), + (Expression expr) -> new NumberSortScript.Factory() { + @Override + public NumberSortScript.LeafFactory newFactory(Map params, SearchLookup lookup) { + return newSortScript(expr, lookup, params); + } + + @Override + public boolean isResultDeterministic() { + return true; + } + }, FieldScript.CONTEXT, - (Expression expr) -> (FieldScript.Factory) (p, lookup) -> newFieldScript(expr, lookup, p) + (Expression expr) -> new FieldScript.Factory() { + @Override + public FieldScript.LeafFactory newFactory(Map params, SearchLookup lookup) { + return newFieldScript(expr, lookup, params); + } + + @Override + public boolean isResultDeterministic() { + return true; + } + } ); @Override @@ -109,7 +158,7 @@ public String getType() { } @Override - public T compile( + public T compile( String scriptName, String scriptSource, ScriptContext context, diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java index f14dd35d339fd..391b6a5c2d20c 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java @@ -21,9 +21,8 @@ import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheException; import com.github.mustachejava.MustacheFactory; - -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.SpecialPermission; @@ -32,7 +31,6 @@ import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; import org.elasticsearch.script.ScriptException; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.TemplateScript; import java.io.Reader; @@ -65,7 +63,7 @@ public final class MustacheScriptEngine implements ScriptEngine { * @return a compiled template object for later execution. * */ @Override - public T compile( + public T compile( String templateName, String templateSource, ScriptContext context, diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java index 45db4428aeced..67aedf113c32c 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java @@ -28,7 +28,6 @@ import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; import org.elasticsearch.script.ScriptException; -import org.elasticsearch.script.ScriptFactory; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -123,7 +122,7 @@ public String getType() { } @Override - public T compile( + public T compile( String scriptName, String scriptSource, ScriptContext context, @@ -169,7 +168,7 @@ public Set> getSupportedContexts() { * @param The factory class. * @return A factory class that will return script instances. */ - private Type generateStatefulFactory( + private Type generateStatefulFactory( Loader loader, ScriptContext context, Set extractedVariables @@ -275,7 +274,7 @@ private Type generateStatefulFactory( * @param The factory class. * @return A factory class that will return script instances. */ - private T generateFactory( + private T generateFactory( Loader loader, ScriptContext context, Set extractedVariables, diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java index e38d87e69ccb3..b6bb1a842640d 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java @@ -75,7 +75,6 @@ import org.elasticsearch.script.ScoreScript; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; import org.elasticsearch.threadpool.ThreadPool; @@ -416,7 +415,7 @@ public Map getParams() { public abstract Object execute(); - public interface Factory extends ScriptFactory { + public interface Factory { PainlessTestScript newInstance(Map params); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BaseClassTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BaseClassTests.java index 96790301139f3..4c6aa890ba1f8 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BaseClassTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BaseClassTests.java @@ -21,7 +21,6 @@ import org.elasticsearch.painless.spi.Whitelist; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; import java.util.Collections; import java.util.HashMap; @@ -66,7 +65,7 @@ protected Map, List> scriptContexts() { public abstract static class Gets { - public interface Factory extends ScriptFactory { + public interface Factory { Gets newInstance(String testString, int testInt, Map params); } @@ -112,7 +111,7 @@ public void testGets() throws Exception { } public abstract static class NoArgs { - public interface Factory extends ScriptFactory { + public interface Factory { NoArgs newInstance(); } @@ -138,7 +137,7 @@ public void testNoArgs() throws Exception { } public abstract static class OneArg { - public interface Factory extends ScriptFactory { + public interface Factory { OneArg newInstance(); } @@ -155,7 +154,7 @@ public void testOneArg() throws Exception { } public abstract static class ArrayArg { - public interface Factory extends ScriptFactory { + public interface Factory { ArrayArg newInstance(); } @@ -172,7 +171,7 @@ public void testArrayArg() throws Exception { } public abstract static class PrimitiveArrayArg { - public interface Factory extends ScriptFactory { + public interface Factory { PrimitiveArrayArg newInstance(); } @@ -189,7 +188,7 @@ public void testPrimitiveArrayArg() throws Exception { } public abstract static class DefArrayArg { - public interface Factory extends ScriptFactory { + public interface Factory { DefArrayArg newInstance(); } @@ -213,7 +212,7 @@ public void testDefArrayArg()throws Exception { } public abstract static class ManyArgs { - public interface Factory extends ScriptFactory { + public interface Factory { ManyArgs newInstance(); } @@ -252,7 +251,7 @@ public void testManyArgs() throws Exception { } public abstract static class VarArgs { - public interface Factory extends ScriptFactory { + public interface Factory { VarArgs newInstance(); } @@ -268,7 +267,7 @@ public void testVarArgs() throws Exception { } public abstract static class DefaultMethods { - public interface Factory extends ScriptFactory { + public interface Factory { DefaultMethods newInstance(); } @@ -302,7 +301,7 @@ public void testDefaultMethods() throws Exception { } public abstract static class ReturnsVoid { - public interface Factory extends ScriptFactory { + public interface Factory { ReturnsVoid newInstance(); } @@ -326,7 +325,7 @@ public void testReturnsVoid() throws Exception { } public abstract static class ReturnsPrimitiveBoolean { - public interface Factory extends ScriptFactory { + public interface Factory { ReturnsPrimitiveBoolean newInstance(); } @@ -392,7 +391,7 @@ public void testReturnsPrimitiveBoolean() throws Exception { } public abstract static class ReturnsPrimitiveInt { - public interface Factory extends ScriptFactory { + public interface Factory { ReturnsPrimitiveInt newInstance(); } @@ -456,7 +455,7 @@ public void testReturnsPrimitiveInt() throws Exception { } public abstract static class ReturnsPrimitiveFloat { - public interface Factory extends ScriptFactory { + public interface Factory { ReturnsPrimitiveFloat newInstance(); } @@ -505,7 +504,7 @@ public void testReturnsPrimitiveFloat() throws Exception { } public abstract static class ReturnsPrimitiveDouble { - public interface Factory extends ScriptFactory { + public interface Factory { ReturnsPrimitiveDouble newInstance(); } @@ -568,7 +567,7 @@ public void testReturnsPrimitiveDouble() throws Exception { } public abstract static class NoArgsConstant { - public interface Factory extends ScriptFactory { + public interface Factory { NoArgsConstant newInstance(); } @@ -585,7 +584,7 @@ public void testNoArgsConstant() { } public abstract static class WrongArgsConstant { - public interface Factory extends ScriptFactory { + public interface Factory { WrongArgsConstant newInstance(); } @@ -603,7 +602,7 @@ public void testWrongArgsConstant() { } public abstract static class WrongLengthOfArgConstant { - public interface Factory extends ScriptFactory { + public interface Factory { WrongLengthOfArgConstant newInstance(); } @@ -620,7 +619,7 @@ public void testWrongLengthOfArgConstant() { } public abstract static class UnknownArgType { - public interface Factory extends ScriptFactory { + public interface Factory { UnknownArgType newInstance(); } @@ -637,7 +636,7 @@ public void testUnknownArgType() { } public abstract static class UnknownReturnType { - public interface Factory extends ScriptFactory { + public interface Factory { UnknownReturnType newInstance(); } @@ -654,7 +653,7 @@ public void testUnknownReturnType() { } public abstract static class UnknownArgTypeInArray { - public interface Factory extends ScriptFactory { + public interface Factory { UnknownArgTypeInArray newInstance(); } @@ -671,7 +670,7 @@ public void testUnknownArgTypeInArray() { } public abstract static class TwoExecuteMethods { - public interface Factory extends ScriptFactory { + public interface Factory { TwoExecuteMethods newInstance(); } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java index 72fc01dbf2ce6..e4d1db2243b82 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java @@ -2,7 +2,6 @@ import org.elasticsearch.painless.spi.Whitelist; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; import java.util.ArrayList; import java.util.Collections; @@ -261,7 +260,7 @@ public void testReturnStatement() { } public abstract static class OneArg { - public interface Factory extends ScriptFactory { + public interface Factory { OneArg newInstance(); } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java index d165e0ef76d81..171880abd7907 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java @@ -23,7 +23,6 @@ import org.elasticsearch.painless.spi.WhitelistInstanceBinding; import org.elasticsearch.painless.spi.WhitelistLoader; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; import java.util.ArrayList; import java.util.Collections; @@ -90,7 +89,7 @@ public abstract static class BindingsTestScript { public static final String[] PARAMETERS = { "test", "bound" }; public int getTestValue() {return 7;} public abstract int execute(int test, int bound); - public interface Factory extends ScriptFactory { + public interface Factory { BindingsTestScript newInstance(); } public static final ScriptContext CONTEXT = new ScriptContext<>("bindings_test", Factory.class); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java index 33645e24c0b3c..ffd4df43c9070 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/FactoryTests.java @@ -35,6 +35,7 @@ protected Map, List> scriptContexts() { Map, List> contexts = super.scriptContexts(); contexts.put(StatefulFactoryTestScript.CONTEXT, Whitelist.BASE_WHITELISTS); contexts.put(FactoryTestScript.CONTEXT, Whitelist.BASE_WHITELISTS); + contexts.put(DeterministicFactoryTestScript.CONTEXT, Whitelist.BASE_WHITELISTS); contexts.put(EmptyTestScript.CONTEXT, Whitelist.BASE_WHITELISTS); contexts.put(TemplateScript.CONTEXT, Whitelist.BASE_WHITELISTS); @@ -85,7 +86,7 @@ public interface StatefulFactory { boolean needsD(); } - public interface Factory extends ScriptFactory { + public interface Factory { StatefulFactory newFactory(int x, int y); boolean needsTest(); @@ -138,7 +139,7 @@ public Map getParams() { public static final String[] PARAMETERS = new String[] {"test"}; public abstract Object execute(int test); - public interface Factory extends ScriptFactory { + public interface Factory { FactoryTestScript newInstance(Map params); boolean needsTest(); @@ -149,6 +150,31 @@ public interface Factory extends ScriptFactory { new ScriptContext<>("test", FactoryTestScript.Factory.class); } + public abstract static class DeterministicFactoryTestScript { + private final Map params; + + public DeterministicFactoryTestScript(Map params) { + this.params = params; + } + + public Map getParams() { + return params; + } + + public static final String[] PARAMETERS = new String[] {"test"}; + public abstract Object execute(int test); + + public interface Factory extends ScriptFactory{ + FactoryTestScript newInstance(Map params); + + boolean needsTest(); + boolean needsNothing(); + } + + public static final ScriptContext CONTEXT = + new ScriptContext<>("test", DeterministicFactoryTestScript.Factory.class); + } + public void testFactory() { FactoryTestScript.Factory factory = scriptEngine.compile("factory_test", "test + params.get('test')", FactoryTestScript.CONTEXT, Collections.emptyMap()); @@ -163,26 +189,26 @@ public void testFactory() { } public void testDeterministic() { - FactoryTestScript.Factory factory = + DeterministicFactoryTestScript.Factory factory = scriptEngine.compile("deterministic_test", "Integer.parseInt('123')", - FactoryTestScript.CONTEXT, Collections.emptyMap()); + DeterministicFactoryTestScript.CONTEXT, Collections.emptyMap()); assertTrue(factory.isResultDeterministic()); assertEquals(123, factory.newInstance(Collections.emptyMap()).execute(0)); } public void testNotDeterministic() { - FactoryTestScript.Factory factory = + DeterministicFactoryTestScript.Factory factory = scriptEngine.compile("not_deterministic_test", "Math.random()", - FactoryTestScript.CONTEXT, Collections.emptyMap()); + DeterministicFactoryTestScript.CONTEXT, Collections.emptyMap()); assertFalse(factory.isResultDeterministic()); Double d = (Double)factory.newInstance(Collections.emptyMap()).execute(0); assertTrue(d >= 0.0 && d <= 1.0); } public void testMixedDeterministicIsNotDeterministic() { - FactoryTestScript.Factory factory = + DeterministicFactoryTestScript.Factory factory = scriptEngine.compile("not_deterministic_test", "Integer.parseInt('123') + Math.random()", - FactoryTestScript.CONTEXT, Collections.emptyMap()); + DeterministicFactoryTestScript.CONTEXT, Collections.emptyMap()); assertFalse(factory.isResultDeterministic()); Double d = (Double)factory.newInstance(Collections.emptyMap()).execute(0); assertTrue(d >= 123.0 && d <= 124.0); @@ -192,7 +218,7 @@ public abstract static class EmptyTestScript { public static final String[] PARAMETERS = {}; public abstract Object execute(); - public interface Factory extends ScriptFactory { + public interface Factory { EmptyTestScript newInstance(); } diff --git a/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java b/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java index 5846f000f5e2d..5b9a357cff2f0 100644 --- a/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java +++ b/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java @@ -62,7 +62,7 @@ public String getType() { } @Override - public T compile( + public T compile( String scriptName, String scriptSource, ScriptContext context, @@ -92,7 +92,8 @@ public Set> getSupportedContexts() { return Set.of(ScoreScript.CONTEXT); } - private static class PureDfFactory implements ScoreScript.Factory { + private static class PureDfFactory implements ScoreScript.Factory, + ScriptFactory { @Override public boolean isResultDeterministic() { // PureDfLeafFactory only uses deterministic APIs, this diff --git a/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java b/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java index 56161d8f2fa34..12b2da120f63e 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java @@ -323,11 +323,9 @@ public Index index() { } /** Compile script using script service */ - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { FactoryType factory = scriptService.compile(script, context); - // TODO(stu): can check `factory instanceof ScriptFactory && ((ScriptFactory) factory).isResultDeterministic() == false` - // to avoid being so intrusive - if (factory.isResultDeterministic() == false) { + if (factory instanceof ScriptFactory && ((ScriptFactory) factory).isResultDeterministic() == false) { failIfFrozen(); } return factory; diff --git a/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java b/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java index 3c765439223d2..a8e2fad7cdcda 100644 --- a/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java +++ b/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java @@ -48,7 +48,7 @@ public Map getParams() { public abstract boolean execute(); - public interface Factory extends ScriptFactory { + public interface Factory { BucketAggregationSelectorScript newInstance(Map params); } } diff --git a/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java b/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java index 44d87cfe6aba2..27ce29b95dc50 100644 --- a/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java +++ b/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java @@ -45,7 +45,7 @@ public Map getParams() { public abstract boolean execute(Map ctx); - public interface Factory extends ScriptFactory { + public interface Factory { IngestConditionalScript newInstance(Map params); } } diff --git a/server/src/main/java/org/elasticsearch/script/IngestScript.java b/server/src/main/java/org/elasticsearch/script/IngestScript.java index 7104ed7d9b0d2..f357394ed31f0 100644 --- a/server/src/main/java/org/elasticsearch/script/IngestScript.java +++ b/server/src/main/java/org/elasticsearch/script/IngestScript.java @@ -46,7 +46,7 @@ public Map getParams() { public abstract void execute(Map ctx); - public interface Factory extends ScriptFactory { + public interface Factory { IngestScript newInstance(Map params); } } diff --git a/server/src/main/java/org/elasticsearch/script/ScriptContext.java b/server/src/main/java/org/elasticsearch/script/ScriptContext.java index 4e927da09d118..081a26d1e511a 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptContext.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptContext.java @@ -54,7 +54,7 @@ * If the variable name starts with an underscore, for example, {@code _score}, the needs method would * be {@code boolean needs_score()}. */ -public final class ScriptContext { +public final class ScriptContext { /** A unique identifier for this context. */ public final String name; diff --git a/server/src/main/java/org/elasticsearch/script/ScriptEngine.java b/server/src/main/java/org/elasticsearch/script/ScriptEngine.java index 4c38ae5c6e19c..78a012700c2b2 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptEngine.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptEngine.java @@ -42,7 +42,7 @@ public interface ScriptEngine extends Closeable { * @param params compile-time parameters (such as flags to the compiler) * @return A compiled script of the FactoryType from {@link ScriptContext} */ - FactoryType compile( + FactoryType compile( String name, String code, ScriptContext context, diff --git a/server/src/main/java/org/elasticsearch/script/ScriptService.java b/server/src/main/java/org/elasticsearch/script/ScriptService.java index 77375838e7f16..a1788ee74a163 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptService.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptService.java @@ -284,7 +284,7 @@ void setMaxCompilationRate(Tuple newRate) { * * @return a compiled script which may be used to construct instances of a script for the given context */ - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { Objects.requireNonNull(script); Objects.requireNonNull(context); diff --git a/server/src/main/java/org/elasticsearch/script/TemplateScript.java b/server/src/main/java/org/elasticsearch/script/TemplateScript.java index f7cf4590387d8..c053cf2b509d0 100644 --- a/server/src/main/java/org/elasticsearch/script/TemplateScript.java +++ b/server/src/main/java/org/elasticsearch/script/TemplateScript.java @@ -41,7 +41,7 @@ public Map getParams() { /** Run a template and return the resulting string, encoded in utf8 bytes. */ public abstract String execute(); - public interface Factory extends ScriptFactory { + public interface Factory { TemplateScript newInstance(Map params); } diff --git a/server/src/main/java/org/elasticsearch/script/UpdateScript.java b/server/src/main/java/org/elasticsearch/script/UpdateScript.java index 5dd08d5b602dd..ae0827ff83934 100644 --- a/server/src/main/java/org/elasticsearch/script/UpdateScript.java +++ b/server/src/main/java/org/elasticsearch/script/UpdateScript.java @@ -58,7 +58,7 @@ public Map getCtx() { public abstract void execute(); - public interface Factory extends ScriptFactory { + public interface Factory { UpdateScript newInstance(Map params, Map ctx); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java index cfcc1a9cd3eb0..763d10ddf30e8 100644 --- a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java @@ -35,7 +35,6 @@ import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; import org.elasticsearch.test.AbstractQueryTestCase; @@ -397,7 +396,7 @@ public boolean execute(Interval interval) { ScriptService scriptService = new ScriptService(Settings.EMPTY, Collections.emptyMap(), Collections.emptyMap()){ @Override @SuppressWarnings("unchecked") - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { assertEquals(IntervalFilterScript.CONTEXT, context); assertEquals(new Script("interval.start > 3"), script); return (FactoryType) factory; diff --git a/server/src/test/java/org/elasticsearch/script/ScriptContextTests.java b/server/src/test/java/org/elasticsearch/script/ScriptContextTests.java index dc77fb0126262..157b0969ae813 100644 --- a/server/src/test/java/org/elasticsearch/script/ScriptContextTests.java +++ b/server/src/test/java/org/elasticsearch/script/ScriptContextTests.java @@ -23,28 +23,28 @@ public class ScriptContextTests extends ESTestCase { - public interface TwoNewInstance extends ScriptFactory { + public interface TwoNewInstance { String newInstance(int foo, int bar); String newInstance(int foo); - interface StatefulFactory extends ScriptFactory { + interface StatefulFactory { TwoNewInstance newFactory(); } } - public interface TwoNewFactory extends ScriptFactory { + public interface TwoNewFactory { String newFactory(int foo, int bar); String newFactory(int foo); } - public interface MissingNewInstance extends ScriptFactory { + public interface MissingNewInstance { String typoNewInstanceMethod(int foo); } public interface DummyScript { int execute(int foo); - interface Factory extends ScriptFactory { + interface Factory { DummyScript newInstance(); } } @@ -54,7 +54,7 @@ public interface DummyStatefulScript { interface StatefulFactory { DummyStatefulScript newInstance(); } - interface Factory extends ScriptFactory { + interface Factory { StatefulFactory newFactory(); } } diff --git a/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java b/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java index 6e720b480934b..38139103ed2ab 100644 --- a/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java +++ b/server/src/test/java/org/elasticsearch/script/ScriptLanguagesInfoTests.java @@ -75,7 +75,7 @@ private ScriptService getMockScriptService(Settings settings) { } - public interface MiscContext extends ScriptFactory { + public interface MiscContext { void execute(); Object newInstance(); } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java index 406a96e3c34cd..6c2981c4ad79d 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java @@ -1037,7 +1037,6 @@ public void testEmptyAggregation() throws Exception { * Make sure that a request using a deterministic script gets cached and nondeterministic scripts do not get cached. */ public void testScriptCaching() throws Exception { - // TODO(stu): add non-determinism in init, agg, combine and reduce, ensure not cached Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state['count'] = 1", Collections.emptyMap()); Script combineScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "no-op aggregation", Collections.emptyMap()); diff --git a/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java b/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java index 2e61882256b77..dda35ff803c1d 100644 --- a/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java +++ b/server/src/test/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java @@ -34,7 +34,6 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; @@ -76,7 +75,7 @@ public String getType() { } @Override - public T compile( + public T compile( String scriptName, String scriptSource, ScriptContext context, diff --git a/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java b/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java index 5434f05c9aa00..fc2826258a47a 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java @@ -34,7 +34,6 @@ import org.elasticsearch.plugins.ScriptPlugin; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.TemplateScript; import org.elasticsearch.search.suggest.phrase.DirectCandidateGeneratorBuilder; import org.elasticsearch.search.suggest.phrase.Laplace; @@ -1138,7 +1137,7 @@ public String getType() { } @Override - public T compile( + public T compile( String scriptName, String scriptSource, ScriptContext context, diff --git a/test/framework/src/main/java/org/elasticsearch/ingest/TestTemplateService.java b/test/framework/src/main/java/org/elasticsearch/ingest/TestTemplateService.java index b5fcb2a37d7e3..5bbf39d8fdc17 100644 --- a/test/framework/src/main/java/org/elasticsearch/ingest/TestTemplateService.java +++ b/test/framework/src/main/java/org/elasticsearch/ingest/TestTemplateService.java @@ -23,7 +23,6 @@ import org.elasticsearch.script.MockScriptEngine; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.TemplateScript; @@ -49,7 +48,7 @@ private TestTemplateService(boolean compilationException) { } @Override - public FactoryType compile(Script script, ScriptContext context) { + public FactoryType compile(Script script, ScriptContext context) { if (this.compilationException) { throw new RuntimeException("could not compile script"); } else { diff --git a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java index 3069efbebb451..ab34dcbf09bf5 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java @@ -93,7 +93,7 @@ public String getType() { } @Override - public T compile(String name, String source, ScriptContext context, Map params) { + public T compile(String name, String source, ScriptContext context, Map params) { // Scripts are always resolved using the script's source. For inline scripts, it's easy because they don't have names and the // source is always provided. For stored and file scripts, the source of the script must match the key of a predefined script. MockDeterministicScript script = scripts.get(source); @@ -252,7 +252,6 @@ public double execute(Map params1, double[] values) { @Override public Set> getSupportedContexts() { - // TODO(stu): make part of `compile()` return Set.of( FieldScript.CONTEXT, TermsSetQueryScript.CONTEXT, @@ -395,7 +394,7 @@ public double execute(Query query, Field field, Term term) { } } - public static class MockMetricAggInitScriptFactory implements ScriptedMetricAggContexts.InitScript.Factory, ScriptFactory { + public static class MockMetricAggInitScriptFactory implements ScriptedMetricAggContexts.InitScript.Factory { private final MockDeterministicScript script; MockMetricAggInitScriptFactory(MockDeterministicScript script) { this.script = script; } @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } @@ -428,7 +427,7 @@ public void execute() { } } - public static class MockMetricAggMapScriptFactory implements ScriptedMetricAggContexts.MapScript.Factory, ScriptFactory { + public static class MockMetricAggMapScriptFactory implements ScriptedMetricAggContexts.MapScript.Factory { private final MockDeterministicScript script; MockMetricAggMapScriptFactory(MockDeterministicScript script) { this.script = script; } @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } @@ -476,7 +475,7 @@ public void execute() { } } - public static class MockMetricAggCombineScriptFactory implements ScriptedMetricAggContexts.CombineScript.Factory, ScriptFactory { + public static class MockMetricAggCombineScriptFactory implements ScriptedMetricAggContexts.CombineScript.Factory { private final MockDeterministicScript script; MockMetricAggCombineScriptFactory(MockDeterministicScript script) { this.script = script; } @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } @@ -508,7 +507,7 @@ public Object execute() { } } - public static class MockMetricAggReduceScriptFactory implements ScriptedMetricAggContexts.ReduceScript.Factory, ScriptFactory { + public static class MockMetricAggReduceScriptFactory implements ScriptedMetricAggContexts.ReduceScript.Factory { private final MockDeterministicScript script; MockMetricAggReduceScriptFactory(MockDeterministicScript script) { this.script = script; } @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } @@ -584,7 +583,7 @@ public void setScorer(Scorable scorer) { } } - class MockAggregationScript implements AggregationScript.Factory, ScriptFactory { + class MockAggregationScript implements AggregationScript.Factory { private final MockDeterministicScript script; MockAggregationScript(MockDeterministicScript script) { this.script = script; } @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } @@ -615,7 +614,7 @@ public boolean needs_score() { } } - class MockSignificantTermsHeuristicScoreScript implements SignificantTermsHeuristicScoreScript.Factory, ScriptFactory { + class MockSignificantTermsHeuristicScoreScript implements SignificantTermsHeuristicScoreScript.Factory { private final MockDeterministicScript script; MockSignificantTermsHeuristicScoreScript(MockDeterministicScript script) { this.script = script; } @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } @@ -631,7 +630,7 @@ public double execute(Map vars) { } } - class MockFieldScriptFactory implements FieldScript.Factory, ScriptFactory { + class MockFieldScriptFactory implements FieldScript.Factory { private final MockDeterministicScript script; MockFieldScriptFactory(MockDeterministicScript script) { this.script = script; } @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } @@ -650,7 +649,7 @@ public Object execute() { } } - class MockStringSortScriptFactory implements StringSortScript.Factory, ScriptFactory { + class MockStringSortScriptFactory implements StringSortScript.Factory { private final MockDeterministicScript script; MockStringSortScriptFactory(MockDeterministicScript script) { this.script = script; } @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/script/MockMustacheScriptEngine.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/script/MockMustacheScriptEngine.java index 16a95f0accdac..4f9b125a9fd6b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/script/MockMustacheScriptEngine.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/script/MockMustacheScriptEngine.java @@ -39,7 +39,7 @@ public String getType() { } @Override - public T compile(String name, String script, ScriptContext context, Map params) { + public T compile(String name, String script, ScriptContext context, Map params) { if (script.contains("{{") && script.contains("}}")) { throw new IllegalArgumentException("Fix your test to not rely on mustache"); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/monitoring/test/MockPainlessScriptEngine.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/monitoring/test/MockPainlessScriptEngine.java index 254542f355e5d..2052cebe1d0e9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/monitoring/test/MockPainlessScriptEngine.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/monitoring/test/MockPainlessScriptEngine.java @@ -11,7 +11,6 @@ import org.elasticsearch.script.ScoreScript; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; -import org.elasticsearch.script.ScriptFactory; import java.util.Collection; import java.util.Collections; @@ -43,7 +42,7 @@ public String getType() { } @Override - public T compile(String name, String script, ScriptContext context, Map options) { + public T compile(String name, String script, ScriptContext context, Map options) { if (context.instanceClazz.equals(ScoreScript.class)) { return context.factoryClazz.cast(new MockScoreScript(p -> 0.0)); } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/condition/WatcherConditionScript.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/condition/WatcherConditionScript.java index f5376aa424692..1a5c8718bbd45 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/condition/WatcherConditionScript.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/condition/WatcherConditionScript.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.watcher.condition; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.xpack.core.watcher.execution.WatchExecutionContext; import org.elasticsearch.xpack.watcher.support.Variables; @@ -38,7 +37,7 @@ public Map getCtx() { return ctx; } - public interface Factory extends ScriptFactory { + public interface Factory { WatcherConditionScript newInstance(Map params, WatchExecutionContext watcherContext); } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transform/script/WatcherTransformScript.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transform/script/WatcherTransformScript.java index 3ef1f87cd608c..57ee1e9f35c5d 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transform/script/WatcherTransformScript.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transform/script/WatcherTransformScript.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.watcher.transform.script; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptFactory; import org.elasticsearch.xpack.core.watcher.execution.WatchExecutionContext; import org.elasticsearch.xpack.core.watcher.watch.Payload; import org.elasticsearch.xpack.watcher.support.Variables; @@ -39,7 +38,7 @@ public Map getCtx() { return ctx; } - public interface Factory extends ScriptFactory { + public interface Factory { WatcherTransformScript newInstance(Map params, WatchExecutionContext watcherContext, Payload payload); } From 76dba298f769c4906cb21aaad28970729e6ee0a7 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Thu, 19 Dec 2019 12:22:54 -0700 Subject: [PATCH 278/686] Modify proxy mode to support a single address (#50391) Currently, the remote proxy connection mode uses a list setting for the proxy address. This commit modifies this so that the setting is proxy_address and only supports a single remote proxy address. --- .../15_connection_mode_configuration.yml | 18 +-- .../test/multi_cluster/20_info.yml | 8 +- .../transport/ProxyConnectionStrategy.java | 95 +++++------- .../transport/RemoteConnectionStrategy.java | 6 +- .../ProxyConnectionStrategyTests.java | 144 +++++++----------- .../RemoteClusterConnectionTests.java | 10 +- .../70_connection_mode_configuration.yml | 18 +-- 7 files changed, 121 insertions(+), 178 deletions(-) diff --git a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/15_connection_mode_configuration.yml b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/15_connection_mode_configuration.yml index 5606a08cd261e..05185cb3e3328 100644 --- a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/15_connection_mode_configuration.yml +++ b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/15_connection_mode_configuration.yml @@ -14,7 +14,7 @@ transient: cluster.remote.test_remote_cluster.mode: "proxy" cluster.remote.test_remote_cluster.node_connections: "5" - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } @@ -29,7 +29,7 @@ transient: cluster.remote.test_remote_cluster.mode: "proxy" cluster.remote.test_remote_cluster.seeds: $remote_ip - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } @@ -64,12 +64,12 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip cluster.remote.test_remote_cluster.seeds: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.proxy_addresses\" cannot be + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.proxy_address\" cannot be used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=PROXY, configured=SNIFF]" } --- @@ -87,11 +87,11 @@ transient: cluster.remote.test_remote_cluster.mode: "proxy" cluster.remote.test_remote_cluster.proxy_socket_connections: "3" - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "proxy"} - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_socket_connections: "3"} - - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_addresses: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_address: $remote_ip} - do: search: @@ -179,7 +179,7 @@ body: transient: cluster.remote.test_remote_cluster.mode: "proxy" - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } @@ -193,10 +193,10 @@ transient: cluster.remote.test_remote_cluster.mode: "proxy" cluster.remote.test_remote_cluster.seeds: null - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "proxy"} - - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_addresses: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_address: $remote_ip} - do: search: diff --git a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml index 10378aaeda125..0e5236f9b1171 100644 --- a/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml +++ b/qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/20_info.yml @@ -70,17 +70,17 @@ cluster.remote.test_remote_cluster.seeds: null cluster.remote.test_remote_cluster.node_connections: null cluster.remote.test_remote_cluster.proxy_socket_connections: "10" - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "proxy"} - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_socket_connections: "10"} - - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_addresses: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_address: $remote_ip} - do: cluster.remote_info: {} - match: { test_remote_cluster.connected: true } - - match: { test_remote_cluster.addresses.0: $remote_ip } + - match: { test_remote_cluster.address: $remote_ip } - gt: { test_remote_cluster.num_sockets_connected: 0} - match: { test_remote_cluster.max_socket_connections: 10} - match: { test_remote_cluster.initial_connect_timeout: "30s" } @@ -92,7 +92,7 @@ transient: cluster.remote.test_remote_cluster.mode: null cluster.remote.test_remote_cluster.proxy_socket_connections: null - cluster.remote.test_remote_cluster.proxy_addresses: null + cluster.remote.test_remote_cluster.proxy_address: null --- "skip_unavailable is returned as part of _remote/info response": diff --git a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java index dabdd771afaed..78c9f5f28154d 100644 --- a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java @@ -27,6 +27,7 @@ import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodeRole; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -37,18 +38,14 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; import static org.elasticsearch.common.settings.Setting.boolSetting; @@ -57,18 +54,16 @@ public class ProxyConnectionStrategy extends RemoteConnectionStrategy { /** - * A list of addresses for remote cluster connections. The connections will be opened to the configured addresses in a round-robin - * fashion. + * The remote address for the proxy. The connections will be opened to the configured address. */ - public static final Setting.AffixSetting> REMOTE_CLUSTER_ADDRESSES = Setting.affixKeySetting( + public static final Setting.AffixSetting REMOTE_CLUSTER_ADDRESSES = Setting.affixKeySetting( "cluster.remote.", - "proxy_addresses", - (ns, key) -> Setting.listSetting(key, Collections.emptyList(), s -> { - // validate address - parsePort(s); - return s; - }, new StrategyValidator<>(ns, key, ConnectionStrategy.PROXY), - Setting.Property.Dynamic, Setting.Property.NodeScope)); + "proxy_address", + (ns, key) -> Setting.simpleString(key, new StrategyValidator<>(ns, key, ConnectionStrategy.PROXY, s -> { + if (Strings.hasLength(s)) { + parsePort(s); + } + }), Setting.Property.Dynamic, Setting.Property.NodeScope)); /** * The maximum number of socket connections that will be established to a remote cluster. The default is 18. @@ -95,9 +90,9 @@ public class ProxyConnectionStrategy extends RemoteConnectionStrategy { private final int maxNumConnections; private final AtomicLong counter = new AtomicLong(0); - private final List configuredAddresses; + private final String configuredAddress; private final boolean includeServerName; - private final List> addresses; + private final Supplier address; private final AtomicReference remoteClusterName = new AtomicReference<>(); private final ConnectionProfile profile; private final ConnectionManager.ConnectionValidator clusterNameValidator; @@ -114,28 +109,26 @@ public class ProxyConnectionStrategy extends RemoteConnectionStrategy { } ProxyConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, - int maxNumConnections, List configuredAddresses) { - this(clusterAlias, transportService, connectionManager, maxNumConnections, configuredAddresses, - configuredAddresses.stream().map(address -> - (Supplier) () -> resolveAddress(address)).collect(Collectors.toList()), false); + int maxNumConnections, String configuredAddress) { + this(clusterAlias, transportService, connectionManager, maxNumConnections, configuredAddress, + () -> resolveAddress(configuredAddress), false); } ProxyConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, - int maxNumConnections, List configuredAddresses, boolean includeServerName) { - this(clusterAlias, transportService, connectionManager, maxNumConnections, configuredAddresses, - configuredAddresses.stream().map(address -> - (Supplier) () -> resolveAddress(address)).collect(Collectors.toList()), includeServerName); + int maxNumConnections, String configuredAddress, boolean includeServerName) { + this(clusterAlias, transportService, connectionManager, maxNumConnections, configuredAddress, + () -> resolveAddress(configuredAddress), includeServerName); } ProxyConnectionStrategy(String clusterAlias, TransportService transportService, RemoteConnectionManager connectionManager, - int maxNumConnections, List configuredAddresses, List> addresses, + int maxNumConnections, String configuredAddress, Supplier address, boolean includeServerName) { super(clusterAlias, transportService, connectionManager); this.maxNumConnections = maxNumConnections; - this.configuredAddresses = configuredAddresses; + this.configuredAddress = configuredAddress; this.includeServerName = includeServerName; - assert addresses.isEmpty() == false : "Cannot use proxy connection strategy with no configured addresses"; - this.addresses = addresses; + assert Strings.isEmpty(configuredAddress) == false : "Cannot use proxy connection strategy with no configured addresses"; + this.address = address; // TODO: Move into the ConnectionManager this.profile = new ConnectionProfile.Builder() .addConnections(1, TransportRequestOptions.Type.REG, TransportRequestOptions.Type.PING) @@ -172,9 +165,9 @@ protected boolean shouldOpenMoreConnections() { @Override protected boolean strategyMustBeRebuilt(Settings newSettings) { - List addresses = REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias).get(newSettings); + String address = REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias).get(newSettings); int numOfSockets = REMOTE_SOCKET_CONNECTIONS.getConcreteSettingForNamespace(clusterAlias).get(newSettings); - return numOfSockets != maxNumConnections || addressesChanged(configuredAddresses, addresses); + return numOfSockets != maxNumConnections || configuredAddress.equals(address) == false; } @Override @@ -189,7 +182,7 @@ protected void connectImpl(ActionListener listener) { @Override public RemoteConnectionInfo.ModeInfo getModeInfo() { - return new ProxyModeInfo(configuredAddresses, maxNumConnections, connectionManager.size()); + return new ProxyModeInfo(configuredAddress, maxNumConnections, connectionManager.size()); } private void performProxyConnectionProcess(ActionListener listener) { @@ -198,7 +191,7 @@ private void performProxyConnectionProcess(ActionListener listener) { private void openConnections(ActionListener finished, int attemptNumber) { if (attemptNumber <= MAX_CONNECT_ATTEMPTS_PER_RUN) { - List resolved = addresses.stream().map(Supplier::get).collect(Collectors.toList()); + TransportAddress resolved = address.get(); int remaining = maxNumConnections - connectionManager.size(); ActionListener compositeListener = new ActionListener<>() { @@ -228,15 +221,14 @@ public void onFailure(Exception e) { for (int i = 0; i < remaining; ++i) { - TransportAddress address = nextAddress(resolved); - String id = clusterAlias + "#" + address; + String id = clusterAlias + "#" + resolved; Map attributes; if (includeServerName) { - attributes = Collections.singletonMap("server_name", address.address().getHostString()); + attributes = Collections.singletonMap("server_name", resolved.address().getHostString()); } else { attributes = Collections.emptyMap(); } - DiscoveryNode node = new DiscoveryNode(id, address, attributes, DiscoveryNodeRole.BUILT_IN_ROLES, + DiscoveryNode node = new DiscoveryNode(id, resolved, attributes, DiscoveryNodeRole.BUILT_IN_ROLES, Version.CURRENT.minimumCompatibilityVersion()); connectionManager.connectToNode(node, profile, clusterNameValidator, new ActionListener<>() { @@ -248,7 +240,7 @@ public void onResponse(Void v) { @Override public void onFailure(Exception e) { logger.debug(new ParameterizedMessage("failed to open remote connection [remote cluster: {}, address: {}]", - clusterAlias, address), e); + clusterAlias, resolved), e); compositeListener.onFailure(e); } }); @@ -276,40 +268,27 @@ private static TransportAddress resolveAddress(String address) { return new TransportAddress(parseConfiguredAddress(address)); } - private boolean addressesChanged(final List oldAddresses, final List newAddresses) { - if (oldAddresses.size() != newAddresses.size()) { - return true; - } - Set oldSeeds = new HashSet<>(oldAddresses); - Set newSeeds = new HashSet<>(newAddresses); - return oldSeeds.equals(newSeeds) == false; - } - static class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { - private final List addresses; + private final String address; private final int maxSocketConnections; private final int numSocketsConnected; - ProxyModeInfo(List addresses, int maxSocketConnections, int numSocketsConnected) { - this.addresses = addresses; + ProxyModeInfo(String address, int maxSocketConnections, int numSocketsConnected) { + this.address = address; this.maxSocketConnections = maxSocketConnections; this.numSocketsConnected = numSocketsConnected; } private ProxyModeInfo(StreamInput input) throws IOException { - addresses = Arrays.asList(input.readStringArray()); + address = input.readString(); maxSocketConnections = input.readVInt(); numSocketsConnected = input.readVInt(); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startArray("addresses"); - for (String address : addresses) { - builder.value(address); - } - builder.endArray(); + builder.field("address", address); builder.field("num_sockets_connected", numSocketsConnected); builder.field("max_socket_connections", maxSocketConnections); return builder; @@ -317,7 +296,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override public void writeTo(StreamOutput out) throws IOException { - out.writeStringArray(addresses.toArray(new String[0])); + out.writeString(address); out.writeVInt(maxSocketConnections); out.writeVInt(numSocketsConnected); } @@ -344,12 +323,12 @@ public boolean equals(Object o) { ProxyModeInfo otherProxy = (ProxyModeInfo) o; return maxSocketConnections == otherProxy.maxSocketConnections && numSocketsConnected == otherProxy.numSocketsConnected && - Objects.equals(addresses, otherProxy.addresses); + Objects.equals(address, otherProxy.address); } @Override public int hashCode() { - return Objects.hash(addresses, maxSocketConnections, numSocketsConnected); + return Objects.hash(address, maxSocketConnections, numSocketsConnected); } } } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java index 562a6bfbe464f..1ee8fba2bd7eb 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionStrategy.java @@ -26,6 +26,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ContextPreservingActionListener; import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Setting; @@ -159,9 +160,8 @@ public static boolean isConnectionEnabled(String clusterAlias, Settings settings List seeds = SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS.getConcreteSettingForNamespace(clusterAlias).get(settings); return seeds.isEmpty() == false; } else { - List addresses = ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias) - .get(settings); - return addresses.isEmpty() == false; + String address = ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias).get(settings); + return Strings.isEmpty(address) == false; } } diff --git a/server/src/test/java/org/elasticsearch/transport/ProxyConnectionStrategyTests.java b/server/src/test/java/org/elasticsearch/transport/ProxyConnectionStrategyTests.java index 5644ff895725d..1106067055353 100644 --- a/server/src/test/java/org/elasticsearch/transport/ProxyConnectionStrategyTests.java +++ b/server/src/test/java/org/elasticsearch/transport/ProxyConnectionStrategyTests.java @@ -23,7 +23,6 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.AbstractScopedSettings; import org.elasticsearch.common.settings.ClusterSettings; @@ -32,19 +31,16 @@ import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.transport.MockTransportService; -import org.elasticsearch.test.transport.StubbableTransport; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -86,11 +82,9 @@ public MockTransportService startTransport(final String id, final Version versio } } - public void testProxyStrategyWillOpenExpectedNumberOfConnectionsToAddresses() { - try (MockTransportService transport1 = startTransport("node1", Version.CURRENT); - MockTransportService transport2 = startTransport("node2", Version.CURRENT)) { + public void testProxyStrategyWillOpenExpectedNumberOfConnectionsToAddress() { + try (MockTransportService transport1 = startTransport("node1", Version.CURRENT)) { TransportAddress address1 = transport1.boundAddress().publishAddress(); - TransportAddress address2 = transport2.boundAddress().publishAddress(); try (MockTransportService localService = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool)) { localService.start(); @@ -100,16 +94,14 @@ public void testProxyStrategyWillOpenExpectedNumberOfConnectionsToAddresses() { int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, - numOfConnections, addresses(address1, address2))) { + numOfConnections, address1.toString())) { assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); - assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); PlainActionFuture connectFuture = PlainActionFuture.newFuture(); strategy.connect(connectFuture); connectFuture.actionGet(); assertTrue(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); - assertTrue(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); assertEquals(numOfConnections, connectionManager.size()); assertTrue(strategy.assertNoRunningConnections()); } @@ -129,9 +121,10 @@ public void testProxyStrategyWillOpenNewConnectionsOnDisconnect() throws Excepti ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); int numOfConnections = randomIntBetween(4, 8); + try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, - numOfConnections, addresses(address1, address2))) { + numOfConnections, address1.toString(), alternatingResolver(address1, address2), false)) { assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); @@ -143,7 +136,7 @@ numOfConnections, addresses(address1, address2))) { long initialConnectionsToTransport2 = connectionManager.getAllConnectedNodes().stream() .filter(n -> n.getAddress().equals(address2)) .count(); - assertNotEquals(0, initialConnectionsToTransport2); + assertEquals(0, initialConnectionsToTransport2); assertEquals(numOfConnections, connectionManager.size()); assertTrue(strategy.assertNoRunningConnections()); @@ -151,11 +144,12 @@ numOfConnections, addresses(address1, address2))) { assertBusy(() -> { assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); - // More connections now pointing to transport2 + // Connections now pointing to transport2 long finalConnectionsToTransport2 = connectionManager.getAllConnectedNodes().stream() .filter(n -> n.getAddress().equals(address2)) .count(); - assertTrue(finalConnectionsToTransport2 > initialConnectionsToTransport2); + assertNotEquals(0, finalConnectionsToTransport2); + assertEquals(numOfConnections, connectionManager.size()); assertTrue(strategy.assertNoRunningConnections()); }); } @@ -163,56 +157,6 @@ numOfConnections, addresses(address1, address2))) { } } - public void testConnectWithSingleIncompatibleNode() { - Version incompatibleVersion = Version.CURRENT.minimumCompatibilityVersion().minimumCompatibilityVersion(); - try (MockTransportService transport1 = startTransport("compatible-node", Version.CURRENT); - MockTransportService transport2 = startTransport("incompatible-node", incompatibleVersion)) { - TransportAddress address1 = transport1.boundAddress().publishAddress(); - TransportAddress address2 = transport2.boundAddress().publishAddress(); - - try (MockTransportService localService = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool)) { - localService.start(); - localService.acceptIncomingRequests(); - - StubbableTransport stubbableTransport = new StubbableTransport(localService.transport); - ConnectionManager connectionManager = new ConnectionManager(profile, stubbableTransport); - AtomicInteger address1Attempts = new AtomicInteger(0); - AtomicInteger address2Attempts = new AtomicInteger(0); - stubbableTransport.setDefaultConnectBehavior((transport, discoveryNode, profile, listener) -> { - if (discoveryNode.getAddress().equals(address1)) { - address1Attempts.incrementAndGet(); - transport.openConnection(discoveryNode, profile, listener); - } else if (discoveryNode.getAddress().equals(address2)) { - address2Attempts.incrementAndGet(); - transport.openConnection(discoveryNode, profile, listener); - } else { - throw new AssertionError("Unexpected address"); - } - }); - int numOfConnections = 5; - try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); - ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, - numOfConnections, addresses(address1, address2))) { - assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); - assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); - - PlainActionFuture connectFuture = PlainActionFuture.newFuture(); - strategy.connect(connectFuture); - connectFuture.actionGet(); - - assertEquals(4 ,connectionManager.size()); - assertEquals(4 ,connectionManager.getAllConnectedNodes().stream().map(n -> n.getAddress().equals(address1)).count()); - // Three attempts on first round, one attempts on second round, zero attempts on third round - assertEquals(4, address1Attempts.get()); - // Two attempts on first round, one attempt on second round, one attempt on third round - assertEquals(4, address2Attempts.get()); - assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); - assertTrue(strategy.assertNoRunningConnections()); - } - } - } - } - public void testConnectFailsWithIncompatibleNodes() { Version incompatibleVersion = Version.CURRENT.minimumCompatibilityVersion().minimumCompatibilityVersion(); try (MockTransportService transport1 = startTransport("incompatible-node", incompatibleVersion)) { @@ -226,7 +170,7 @@ public void testConnectFailsWithIncompatibleNodes() { int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, - numOfConnections, addresses(address1))) { + numOfConnections, address1.toString())) { PlainActionFuture connectFuture = PlainActionFuture.newFuture(); strategy.connect(connectFuture); @@ -254,9 +198,11 @@ public void testClusterNameValidationPreventConnectingToDifferentClusters() thro ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); int numOfConnections = randomIntBetween(4, 8); + + Supplier resolver = alternatingResolver(address1, address2); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, - numOfConnections, addresses(address1, address2))) { + numOfConnections, address1.toString(), resolver, false)) { assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); @@ -264,12 +210,23 @@ numOfConnections, addresses(address1, address2))) { strategy.connect(connectFuture); connectFuture.actionGet(); - if (connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))) { - assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); - } else { - assertTrue(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); - } - assertTrue(strategy.assertNoRunningConnections()); + assertTrue(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); + assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); + + transport1.close(); + + assertBusy(() -> { + assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); + assertTrue(strategy.assertNoRunningConnections()); + + long finalConnectionsToTransport2 = connectionManager.getAllConnectedNodes().stream() + .filter(n -> n.getAddress().equals(address2)) + .count(); + + // Connections not pointing to transport2 because the cluster name is different + assertEquals(0, finalConnectionsToTransport2); + assertEquals(0, connectionManager.size()); + }); } } } @@ -293,7 +250,7 @@ public void testProxyStrategyWillResolveAddressesEachConnect() throws Exception int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, - numOfConnections, addresses(address), Collections.singletonList(addressSupplier), false)) { + numOfConnections, address.toString(), addressSupplier, false)) { PlainActionFuture connectFuture = PlainActionFuture.newFuture(); strategy.connect(connectFuture); connectFuture.actionGet(); @@ -307,10 +264,8 @@ numOfConnections, addresses(address), Collections.singletonList(addressSupplier } public void testProxyStrategyWillNeedToBeRebuiltIfNumOfSocketsOrAddressesChange() { - try (MockTransportService transport1 = startTransport("node1", Version.CURRENT); - MockTransportService transport2 = startTransport("node2", Version.CURRENT)) { - TransportAddress address1 = transport1.boundAddress().publishAddress(); - TransportAddress address2 = transport2.boundAddress().publishAddress(); + try (MockTransportService remoteTransport = startTransport("node1", Version.CURRENT)) { + TransportAddress remoteAddress = remoteTransport.boundAddress().publishAddress(); try (MockTransportService localService = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool)) { localService.start(); @@ -320,13 +275,12 @@ public void testProxyStrategyWillNeedToBeRebuiltIfNumOfSocketsOrAddressesChange( int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, - numOfConnections, addresses(address1, address2))) { + numOfConnections, remoteAddress.toString())) { PlainActionFuture connectFuture = PlainActionFuture.newFuture(); strategy.connect(connectFuture); connectFuture.actionGet(); - assertTrue(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); - assertTrue(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address2))); + assertTrue(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(remoteAddress))); assertEquals(numOfConnections, connectionManager.size()); assertTrue(strategy.assertNoRunningConnections()); @@ -339,18 +293,18 @@ numOfConnections, addresses(address1, address2))) { Settings noChange = Settings.builder() .put(modeSetting.getKey(), "proxy") - .put(addressesSetting.getKey(), Strings.arrayToCommaDelimitedString(addresses(address1, address2).toArray())) + .put(addressesSetting.getKey(), remoteAddress.toString()) .put(socketConnections.getKey(), numOfConnections) .build(); assertFalse(strategy.shouldRebuildConnection(noChange)); Settings addressesChanged = Settings.builder() .put(modeSetting.getKey(), "proxy") - .put(addressesSetting.getKey(), Strings.arrayToCommaDelimitedString(addresses(address1).toArray())) + .put(addressesSetting.getKey(), remoteAddress.toString()) .build(); assertTrue(strategy.shouldRebuildConnection(addressesChanged)); Settings socketsChanged = Settings.builder() .put(modeSetting.getKey(), "proxy") - .put(addressesSetting.getKey(), Strings.arrayToCommaDelimitedString(addresses(address1, address2).toArray())) + .put(addressesSetting.getKey(), remoteAddress.toString()) .put(socketConnections.getKey(), numOfConnections + 1) .build(); assertTrue(strategy.shouldRebuildConnection(socketsChanged)); @@ -398,14 +352,13 @@ public void testServerNameAttributes() { localService.start(); localService.acceptIncomingRequests(); - ArrayList addresses = new ArrayList<>(); - addresses.add("localhost:" + address1.getPort()); + String serverName = "localhost:" + address1.getPort(); ConnectionManager connectionManager = new ConnectionManager(profile, localService.transport); int numOfConnections = randomIntBetween(4, 8); try (RemoteConnectionManager remoteConnectionManager = new RemoteConnectionManager(clusterAlias, connectionManager); ProxyConnectionStrategy strategy = new ProxyConnectionStrategy(clusterAlias, localService, remoteConnectionManager, - numOfConnections, addresses, true)) { + numOfConnections, serverName, true)) { assertFalse(connectionManager.getAllConnectedNodes().stream().anyMatch(n -> n.getAddress().equals(address1))); PlainActionFuture connectFuture = PlainActionFuture.newFuture(); @@ -422,7 +375,18 @@ public void testServerNameAttributes() { } } - private static List addresses(final TransportAddress... addresses) { - return Arrays.stream(addresses).map(TransportAddress::toString).collect(Collectors.toList()); + private Supplier alternatingResolver(TransportAddress address1, TransportAddress address2) { + // On the first connection round, the connections will be routed to transport1. On the second + //connection round, the connections will be routed to transport2 + AtomicBoolean transportSwitch = new AtomicBoolean(true); + return () -> { + if (transportSwitch.get()) { + transportSwitch.set(false); + return address1; + } else { + transportSwitch.set(true); + return address2; + } + }; } } diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java index 030174dde5f90..195e979c56dc5 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java @@ -356,8 +356,8 @@ public void testRemoteConnectionInfo() throws IOException { modeInfo1 = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 4, 4); modeInfo2 = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 4, 3); } else { - modeInfo1 = new ProxyConnectionStrategy.ProxyModeInfo(remoteAddresses, 18, 18); - modeInfo2 = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 18, 17); + modeInfo1 = new ProxyConnectionStrategy.ProxyModeInfo(remoteAddresses.get(0), 18, 18); + modeInfo2 = new ProxyConnectionStrategy.ProxyModeInfo(remoteAddresses.get(0), 18, 17); } RemoteConnectionInfo stats = @@ -404,7 +404,7 @@ public void testRenderConnectionInfoXContent() throws IOException { if (sniff) { modeInfo = new SniffConnectionStrategy.SniffModeInfo(remoteAddresses, 3, 2); } else { - modeInfo = new ProxyConnectionStrategy.ProxyModeInfo(remoteAddresses, 18, 16); + modeInfo = new ProxyConnectionStrategy.ProxyModeInfo(remoteAddresses.get(0), 18, 16); } RemoteConnectionInfo stats = new RemoteConnectionInfo("test_cluster", modeInfo, TimeValue.timeValueMinutes(30), true); @@ -419,7 +419,7 @@ public void testRenderConnectionInfoXContent() throws IOException { "\"num_nodes_connected\":2,\"max_connections_per_cluster\":3,\"initial_connect_timeout\":\"30m\"," + "\"skip_unavailable\":true}}", Strings.toString(builder)); } else { - assertEquals("{\"test_cluster\":{\"connected\":true,\"mode\":\"proxy\",\"addresses\":[\"seed:1\",\"seed:2\"]," + + assertEquals("{\"test_cluster\":{\"connected\":true,\"mode\":\"proxy\",\"address\":\"seed:1\"," + "\"num_sockets_connected\":16,\"max_socket_connections\":18,\"initial_connect_timeout\":\"30m\"," + "\"skip_unavailable\":true}}", Strings.toString(builder)); } @@ -620,7 +620,7 @@ private Settings buildRandomSettings(String clusterAlias, List addresses private static Settings buildProxySettings(String clusterAlias, List addresses) { Settings.Builder builder = Settings.builder(); builder.put(ProxyConnectionStrategy.REMOTE_CLUSTER_ADDRESSES.getConcreteSettingForNamespace(clusterAlias).getKey(), - Strings.collectionToCommaDelimitedString(addresses)); + addresses.get(0)); builder.put(RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace(clusterAlias).getKey(), "proxy"); return builder.build(); } diff --git a/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/multi_cluster/70_connection_mode_configuration.yml b/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/multi_cluster/70_connection_mode_configuration.yml index 5606a08cd261e..05185cb3e3328 100644 --- a/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/multi_cluster/70_connection_mode_configuration.yml +++ b/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/multi_cluster/70_connection_mode_configuration.yml @@ -14,7 +14,7 @@ transient: cluster.remote.test_remote_cluster.mode: "proxy" cluster.remote.test_remote_cluster.node_connections: "5" - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } @@ -29,7 +29,7 @@ transient: cluster.remote.test_remote_cluster.mode: "proxy" cluster.remote.test_remote_cluster.seeds: $remote_ip - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } @@ -64,12 +64,12 @@ flat_settings: true body: transient: - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip cluster.remote.test_remote_cluster.seeds: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.proxy_addresses\" cannot be + - match: { error.root_cause.0.reason: "Setting \"cluster.remote.test_remote_cluster.proxy_address\" cannot be used with the configured \"cluster.remote.test_remote_cluster.mode\" [required=PROXY, configured=SNIFF]" } --- @@ -87,11 +87,11 @@ transient: cluster.remote.test_remote_cluster.mode: "proxy" cluster.remote.test_remote_cluster.proxy_socket_connections: "3" - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "proxy"} - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_socket_connections: "3"} - - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_addresses: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_address: $remote_ip} - do: search: @@ -179,7 +179,7 @@ body: transient: cluster.remote.test_remote_cluster.mode: "proxy" - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: { status: 400 } - match: { error.root_cause.0.type: "illegal_argument_exception" } @@ -193,10 +193,10 @@ transient: cluster.remote.test_remote_cluster.mode: "proxy" cluster.remote.test_remote_cluster.seeds: null - cluster.remote.test_remote_cluster.proxy_addresses: $remote_ip + cluster.remote.test_remote_cluster.proxy_address: $remote_ip - match: {transient.cluster\.remote\.test_remote_cluster\.mode: "proxy"} - - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_addresses: $remote_ip} + - match: {transient.cluster\.remote\.test_remote_cluster\.proxy_address: $remote_ip} - do: search: From 7038fd2b60ab9f9bf171395b40ecf979f3d7f8d8 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Thu, 19 Dec 2019 12:47:53 -0800 Subject: [PATCH 279/686] Add --data-dir option to run task (#50342) This commit adds a special run.datadir system property that may be passed to `./gradlew run` which sets the root data directory used by the task. While normally overriding the data path is not allowed for test clusters, it is useful when experimenting with the run task. closes #50338 --- build.gradle | 8 +++++ .../testclusters/ElasticsearchNode.java | 6 +++- .../gradle/testclusters/RunTask.java | 29 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index df1641dcc9efe..b03bb7e5a3794 100644 --- a/build.gradle +++ b/build.gradle @@ -434,6 +434,14 @@ class Run extends DefaultTask { public void setDebug(boolean enabled) { project.project(':distribution').run.debug = enabled } + + @Option( + option = "data-dir", + description = "Override the base data directory used by the testcluster" + ) + public void setDataDir(String dataDirStr) { + project.project(':distribution').run.dataDir = dataDirStr + } } task run(type: Run) { diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java index f43f28190da1a..3f54e4e16a149 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java @@ -134,7 +134,6 @@ public class ElasticsearchNode implements TestClusterConfiguration { private final Path confPathRepo; private final Path configFile; - private final Path confPathData; private final Path confPathLogs; private final Path transportPortFile; private final Path httpPortsFile; @@ -151,6 +150,7 @@ public class ElasticsearchNode implements TestClusterConfiguration { private boolean isWorkingDirConfigured = false; private String httpPort = "0"; private String transportPort = "0"; + private Path confPathData; ElasticsearchNode(String path, String name, Project project, ReaperService reaper, File workingDirBase) { this.path = path; @@ -1341,6 +1341,10 @@ void setTransportPort(String transportPort) { this.transportPort = transportPort; } + void setDataPath(Path dataPath) { + this.confPathData = dataPath; + } + @Internal Path getEsStdoutFile() { return esStdoutFile; diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java index 2282651f6d4c1..07e5a6ae221f1 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java @@ -10,9 +10,12 @@ import java.io.Closeable; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; public class RunTask extends DefaultTestClustersTask { @@ -22,6 +25,8 @@ public class RunTask extends DefaultTestClustersTask { private Boolean debug = false; + private Path dataDir = null; + @Option( option = "debug-jvm", description = "Enable debugging configuration, to allow attaching a debugger to elasticsearch." @@ -35,6 +40,19 @@ public Boolean getDebug() { return debug; } + @Option( + option = "data-dir", + description = "Override the base data directory used by the testcluster" + ) + public void setDataDir(String dataDirStr) { + dataDir = Paths.get(dataDirStr).toAbsolutePath(); + } + + @Input + public String getDataDir() { + return dataDir.toString(); + } + @Override public void beforeStart() { int debugPort = 5005; @@ -46,6 +64,14 @@ public void beforeStart() { entry -> entry.getKey().toString().substring(CUSTOM_SETTINGS_PREFIX.length()), entry -> entry.getValue().toString() )); + boolean singleNode = getClusters().stream().flatMap(c -> c.getNodes().stream()).count() == 1; + final Function getDataPath; + if (singleNode) { + getDataPath = n -> dataDir; + } else { + getDataPath = n -> dataDir.resolve(n.getName()); + } + for (ElasticsearchCluster cluster : getClusters()) { cluster.getFirstNode().setHttpPort(String.valueOf(httpPort)); httpPort++; @@ -53,6 +79,9 @@ public void beforeStart() { transportPort++; for (ElasticsearchNode node : cluster.getNodes()) { additionalSettings.forEach(node::setting); + if (dataDir != null) { + node.setDataPath(getDataPath.apply(node)); + } if (debug) { logger.lifecycle( "Running elasticsearch in debug mode, {} suspending until connected on debugPort {}", From a8adee423fa09f83afb53602298c3e5f63aaa1cc Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Thu, 19 Dec 2019 12:55:11 -0800 Subject: [PATCH 280/686] Add shell script for performing atomic pushes across branches (#50401) --- dev-tools/atomic_push.sh | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100755 dev-tools/atomic_push.sh diff --git a/dev-tools/atomic_push.sh b/dev-tools/atomic_push.sh new file mode 100755 index 0000000000000..d1b735f15374e --- /dev/null +++ b/dev-tools/atomic_push.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -e + +if [ "$#" -eq 0 ]; then + printf 'Usage: %s ...\n' "$(basename "$0")" + exit 0; +fi + +REMOTE="$1" + +if ! git ls-remote --exit-code "${REMOTE}" > /dev/null 2>&1; then + echo >&2 "Remote '${REMOTE}' is not valid." + exit 1; +fi + +if [ "$#" -lt 3 ]; then + echo >&2 "You must specify at least two branchs to push." + exit 1; +fi + +if ! git diff-index --quiet HEAD -- ; then + echo >&2 "Found uncommitted changes in working copy." + exit 1; +fi + +ATOMIC_COMMIT_DATE="$(date)" + +for BRANCH in "${@:2}" +do + echo "Validating branch '${BRANCH}'..." + + # Vailidate that script arguments are valid local branch names + if ! git show-ref --verify --quiet "refs/heads/${BRANCH}"; then + echo >&2 "No such branch named '${BRANCH}'." + exit 1; + fi + + # Pull and rebase all branches to ensure we've incorporated any new upstream commits + git checkout --quiet ${BRANCH} + git pull "${REMOTE}" --rebase --quiet + + PENDING_COMMITS=$(git log ${REMOTE}/${BRANCH}..HEAD --oneline | grep "^.*$" -c || true) + + # Ensure that there is exactly 1 unpushed commit in the branch + if [ "${PENDING_COMMITS}" -ne 1 ]; then + echo >&2 "Expected exactly 1 pending commit for branch '${BRANCH}' but ${PENDING_COMMITS} exist." + exit 1; + fi + + # Amend HEAD commit to ensure all branch commit dates are the same + GIT_COMMITTER_DATE="${ATOMIC_COMMIT_DATE}" git commit --amend --no-edit --quiet +done + +echo "Pushing to remote '${REMOTE}'..." +git push --atomic "${REMOTE}" "${@:2}" From 1ff250c5b75037cde323f97bbbeedbd2789db37f Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Thu, 19 Dec 2019 12:56:06 -0800 Subject: [PATCH 281/686] Fix location of README file for rpm/deb The file suffix was changed but package building instructions were missed. --- distribution/packages/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/packages/build.gradle b/distribution/packages/build.gradle index 99de6773c0f92..9e90412cb0c54 100644 --- a/distribution/packages/build.gradle +++ b/distribution/packages/build.gradle @@ -131,7 +131,7 @@ Closure commonPackageConfig(String type, boolean oss, boolean jdk) { with binFiles(type, oss, jdk) } from(rootProject.projectDir) { - include 'README.textile' + include 'README.asciidoc' fileMode 0644 } into('lib') { From 0dbda01f23c56b1842790ffa6b93bb540dc1e2ed Mon Sep 17 00:00:00 2001 From: Marios Trivyzas Date: Fri, 20 Dec 2019 00:19:22 +0200 Subject: [PATCH 282/686] SQL: Fix issue with CAST and NULL checking. (#50371) Previously, during expression optimisation, CAST would be considered nullable if the casted expression resulted to a NULL literal, and would be always non-nullable otherwise. As a result if CASE was wrapped by a null check function like IS NULL or IS NOT NULL it was simplified to TRUE/FALSE, eliminating the actual casting operation. So in case of an expression with an erroneous casting like CAST('foo' AS DATETIME) IS NULL it would be simplified to FALSE instead of throwing an Exception signifying the attempt to cast 'foo' to a DATETIME type. CAST now always returns Nullability.UKNOWN except from the case that its result evaluated to a constant NULL, where it returns Nullability.TRUE. This way the IS NULL/IS NOT NULL don't get simplified to FALSE/TRUE and the CAST actually gets evaluated resulting to a thrown Exception. Fixes: #50191 --- .../sql/expression/function/scalar/Cast.java | 2 +- .../nulls/CheckNullProcessorTests.java | 6 --- .../xpack/sql/optimizer/OptimizerTests.java | 51 ++++++++++++++++++- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java index c3c0da0570903..35bc87eabd7b2 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java @@ -66,7 +66,7 @@ public Nullability nullable() { if (from().isNull()) { return Nullability.TRUE; } - return field().nullable(); + return Nullability.UNKNOWN; } @Override diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/CheckNullProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/CheckNullProcessorTests.java index 01b8ba80de260..4ed35e2f9caa7 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/CheckNullProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/nulls/CheckNullProcessorTests.java @@ -9,15 +9,9 @@ import org.elasticsearch.common.io.stream.Writeable.Reader; import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.sql.expression.function.scalar.Processors; -import org.elasticsearch.xpack.sql.expression.gen.processor.ConstantProcessor; -import org.elasticsearch.xpack.sql.expression.gen.processor.Processor; public class CheckNullProcessorTests extends AbstractWireSerializingTestCase { - private static final Processor FALSE = new ConstantProcessor(false); - private static final Processor TRUE = new ConstantProcessor(true); - private static final Processor NULL = new ConstantProcessor((Object) null); - public static CheckNullProcessor randomProcessor() { return new CheckNullProcessor(randomFrom(CheckNullProcessor.CheckNullOperation.values())); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java index c81b376c0a534..1f45d917dabd2 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.sql.optimizer; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer.PruneSubqueryAliases; import org.elasticsearch.xpack.sql.analysis.index.EsIndex; import org.elasticsearch.xpack.sql.expression.Alias; @@ -438,10 +439,46 @@ public void testNullFoldingIsNull() { assertEquals(false, foldNull.rule(new IsNull(EMPTY, TRUE)).fold()); } + public void testNullFoldingIsNullWithCast() { + FoldNull foldNull = new FoldNull(); + + Cast cast = new Cast(EMPTY, L("foo"), DataType.DATE); + IsNull isNull = new IsNull(EMPTY, cast); + final IsNull isNullOpt = (IsNull) foldNull.rule(isNull); + assertEquals(isNull, isNullOpt); + + SqlIllegalArgumentException sqlIAE = + expectThrows(SqlIllegalArgumentException.class, () -> isNullOpt.asPipe().asProcessor().process(null)); + assertEquals("cannot cast [foo] to [date]: Text 'foo' could not be parsed at index 0", sqlIAE.getMessage()); + + isNull = new IsNull(EMPTY, new Cast(EMPTY, NULL, randomFrom(DataType.values()))); + assertTrue((Boolean) ((IsNull) foldNull.rule(isNull)).asPipe().asProcessor().process(null)); + } + public void testNullFoldingIsNotNull() { FoldNull foldNull = new FoldNull(); assertEquals(true, foldNull.rule(new IsNotNull(EMPTY, TRUE)).fold()); assertEquals(false, foldNull.rule(new IsNotNull(EMPTY, NULL)).fold()); + + Cast cast = new Cast(EMPTY, L("foo"), DataType.DATE); + IsNotNull isNotNull = new IsNotNull(EMPTY, cast); + assertEquals(isNotNull, foldNull.rule(isNotNull)); + } + + public void testNullFoldingIsNotNullWithCast() { + FoldNull foldNull = new FoldNull(); + + Cast cast = new Cast(EMPTY, L("foo"), DataType.DATE); + IsNotNull isNotNull = new IsNotNull(EMPTY, cast); + final IsNotNull isNotNullOpt = (IsNotNull) foldNull.rule(isNotNull); + assertEquals(isNotNull, isNotNullOpt); + + SqlIllegalArgumentException sqlIAE = + expectThrows(SqlIllegalArgumentException.class, () -> isNotNullOpt.asPipe().asProcessor().process(null)); + assertEquals("cannot cast [foo] to [date]: Text 'foo' could not be parsed at index 0", sqlIAE.getMessage()); + + isNotNull = new IsNotNull(EMPTY, new Cast(EMPTY, NULL, randomFrom(DataType.values()))); + assertFalse((Boolean) ((IsNotNull) foldNull.rule(isNotNull)).asPipe().asProcessor().process(null)); } public void testGenericNullableExpression() { @@ -461,6 +498,18 @@ public void testGenericNullableExpression() { assertNullLiteral(rule.rule(new RLike(EMPTY, NULL, "123"))); } + public void testNullFoldingOnCast() { + FoldNull foldNull = new FoldNull(); + + Cast cast = new Cast(EMPTY, NULL, randomFrom(DataType.values())); + assertEquals(Nullability.TRUE, cast.nullable()); + assertNull(foldNull.rule(cast).fold()); + + cast = new Cast(EMPTY, L("foo"), DataType.DATE); + assertEquals(Nullability.UNKNOWN, cast.nullable()); + assertEquals(cast, foldNull.rule(cast)); + } + public void testNullFoldingDoesNotApplyOnLogicalExpressions() { FoldNull rule = new FoldNull(); @@ -1684,4 +1733,4 @@ public void testReplaceAttributesWithTarget() { gt = (GreaterThan) and.left(); assertEquals(a, gt.left()); } -} \ No newline at end of file +} From 4b94ce3af686568e20410d3edf5a4fff01585e12 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Thu, 19 Dec 2019 14:37:25 -0800 Subject: [PATCH 283/686] Disable experimental job trigger Signed-off-by: Mark Vieira --- .ci/jobs.t/elastic+elasticsearch+{branch}+periodic-next.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.ci/jobs.t/elastic+elasticsearch+{branch}+periodic-next.yml b/.ci/jobs.t/elastic+elasticsearch+{branch}+periodic-next.yml index 2a6d868853b03..1ca650911bc11 100644 --- a/.ci/jobs.t/elastic+elasticsearch+{branch}+periodic-next.yml +++ b/.ci/jobs.t/elastic+elasticsearch+{branch}+periodic-next.yml @@ -3,5 +3,4 @@ workspace: /dev/shm/elastic+elasticsearch+%BRANCH%+periodic display-name: "elastic / elasticsearch # %BRANCH% - periodic (experimental)" description: "Periodic testing of the Elasticsearch %BRANCH% branch.\n" - triggers: - - timed: "H H/1 * * *" + triggers: [] From a161296ba0193971a86de0a607cdae05faef3854 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 19 Dec 2019 14:47:28 -0800 Subject: [PATCH 284/686] [DOCS] Updates ML links (#50387) --- docs/reference/intro.asciidoc | 2 +- .../apis/delete-filter.asciidoc | 2 +- .../apis/delete-forecast.asciidoc | 2 +- .../apis/eventresource.asciidoc | 2 +- .../apis/filterresource.asciidoc | 2 +- .../anomaly-detection/apis/forecast.asciidoc | 2 +- .../apis/get-calendar-event.asciidoc | 2 +- .../apis/get-calendar.asciidoc | 2 +- .../apis/get-category.asciidoc | 2 +- .../apis/get-filter.asciidoc | 2 +- .../apis/get-overall-buckets.asciidoc | 2 +- .../apis/post-calendar-event.asciidoc | 2 +- .../apis/put-calendar.asciidoc | 2 +- .../apis/put-filter.asciidoc | 2 +- docs/reference/ml/ml-shared.asciidoc | 22 +++++++++---------- docs/reference/modules/node.asciidoc | 2 +- docs/reference/redirects.asciidoc | 6 ++--- docs/reference/setup/restart-cluster.asciidoc | 2 +- docs/reference/upgrade/close-ml.asciidoc | 2 +- .../upgrade/reindex_upgrade.asciidoc | 4 ++-- 20 files changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/reference/intro.asciidoc b/docs/reference/intro.asciidoc index c16f1bd78a1d5..2591487bd04db 100644 --- a/docs/reference/intro.asciidoc +++ b/docs/reference/intro.asciidoc @@ -168,7 +168,7 @@ embroidery_ needles. ==== But wait, there’s more Want to automate the analysis of your time-series data? You can use -{stack-ov}/ml-overview.html[machine learning] features to create accurate +{ml-docs}/ml-overview.html[machine learning] features to create accurate baselines of normal behavior in your data and identify anomalous patterns. With machine learning, you can detect: diff --git a/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc index d6c563fbe0a88..ffb2f7b894cc4 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc @@ -23,7 +23,7 @@ Deletes a filter. [[ml-delete-filter-desc]] ==== {api-description-title} -This API deletes a {stack-ov}/ml-rules.html[filter]. +This API deletes a {ml-docs}/ml-rules.html[filter]. If a {ml} job references the filter, you cannot delete the filter. You must update or delete the job before you can delete the filter. diff --git a/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc index d723b3fba4877..e269d532b0c27 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc @@ -35,7 +35,7 @@ one or more forecasts before they expire. NOTE: When you delete a job, its associated forecasts are deleted. For more information, see -{stack-ov}/ml-overview.html#ml-forecasting[Forecasting the future]. +{ml-docs}/ml-overview.html#ml-forecasting[Forecasting the future]. [[ml-delete-forecast-path-parms]] ==== {api-path-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/eventresource.asciidoc b/docs/reference/ml/anomaly-detection/apis/eventresource.asciidoc index 4fb179be3cbca..2f3ea4175936a 100644 --- a/docs/reference/ml/anomaly-detection/apis/eventresource.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/eventresource.asciidoc @@ -24,4 +24,4 @@ An events resource has the following properties: in milliseconds since the epoch or ISO 8601 format. For more information, see -{stack-ov}/ml-calendars.html[Calendars and Scheduled Events]. +{ml-docs}/ml-calendars.html[Calendars and scheduled events]. diff --git a/docs/reference/ml/anomaly-detection/apis/filterresource.asciidoc b/docs/reference/ml/anomaly-detection/apis/filterresource.asciidoc index 520a2a99a3c71..39d63aec6a418 100644 --- a/docs/reference/ml/anomaly-detection/apis/filterresource.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/filterresource.asciidoc @@ -14,4 +14,4 @@ A filter resource has the following properties: `items`:: (array of strings) An array of strings which is the filter item list. -For more information, see {stack-ov}/ml-rules.html[Machine learning custom rules]. +For more information, see {ml-docs}/ml-rules.html[Machine learning custom rules]. diff --git a/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc b/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc index 0ec17ea9bd25e..61b8981843184 100644 --- a/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc @@ -23,7 +23,7 @@ Predicts the future behavior of a time series by using its historical behavior. [[ml-forecast-desc]] ==== {api-description-title} -See {stack-ov}/ml-overview.html#ml-forecasting[Forecasting the future]. +See {ml-docs}/ml-overview.html#ml-forecasting[Forecasting the future]. [NOTE] =============================== diff --git a/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc index adf50483e25a5..7824c08e1da41 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc @@ -29,7 +29,7 @@ You can get scheduled event information for a single calendar or for all calendars by using `_all`. For more information, see -{stack-ov}/ml-calendars.html[Calendars and scheduled events]. +{ml-docs}/ml-calendars.html[Calendars and scheduled events]. [[ml-get-calendar-event-path-parms]] ==== {api-path-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc index 9552ba687033c..f2243825b81f0 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc @@ -29,7 +29,7 @@ You can get information for a single calendar or for all calendars by using `_all`. For more information, see -{stack-ov}/ml-calendars.html[Calendars and scheduled events]. +{ml-docs}/ml-calendars.html[Calendars and scheduled events]. [[ml-get-calendar-path-parms]] ==== {api-path-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc index 92beaa0360b5e..97acffbc4a198 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc @@ -37,7 +37,7 @@ The anomaly results from a categorization analysis are available as bucket, influencer, and record results. For example, the results might indicate that at 16:45 there was an unusual count of log message category 11. You can then examine the description and examples of that category. For more information, see -{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. +{ml-docs}/ml-configuring-categories.html[Categorizing log messages]. [[ml-get-category-path-parms]] ==== {api-path-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc index 6ed8ee4fa0c41..6852ca7716d07 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc @@ -26,7 +26,7 @@ Retrieves filters. ==== {api-description-title} You can get a single filter or all filters. For more information, see -{stack-ov}/ml-rules.html[Machine learning custom rules]. +{ml-docs}/ml-rules.html[Machine learning custom rules]. [[ml-get-filter-path-parms]] ==== {api-path-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc index 3c0df917e4b3c..ab1b5588cccfd 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc @@ -36,7 +36,7 @@ by specifying `*` as the ``. By default, an overall bucket has a span equal to the largest bucket span of the specified {anomaly-jobs}. To override that behavior, use the optional `bucket_span` parameter. To learn more about the concept of buckets, see -{stack-ov}/ml-buckets.html[Buckets]. +{ml-docs}/ml-buckets.html[Buckets]. The `overall_score` is calculated by combining the scores of all the buckets within the overall bucket span. First, the maximum `anomaly_score` per diff --git a/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc index d7d3feedfdf3a..c241e7acdeb57 100644 --- a/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc @@ -23,7 +23,7 @@ Posts scheduled events in a calendar. [[ml-post-calendar-event-desc]] ==== {api-description-title} -This API accepts a list of {stack-ov}/ml-calendars.html[scheduled events], each +This API accepts a list of {ml-docs}/ml-calendars.html[scheduled events], each of which must have a start time, end time, and description. [[ml-post-calendar-event-path-parms]] diff --git a/docs/reference/ml/anomaly-detection/apis/put-calendar.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-calendar.asciidoc index 09a65f4300dec..f543c06bd8cb0 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-calendar.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-calendar.asciidoc @@ -24,7 +24,7 @@ Instantiates a calendar. ==== {api-description-title} For more information, see -{stack-ov}/ml-calendars.html[Calendars and scheduled events]. +{ml-docs}/ml-calendars.html[Calendars and scheduled events]. [[ml-put-calendar-path-parms]] ==== {api-path-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc index 3ff63bf927757..86245d84dbb28 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc @@ -23,7 +23,7 @@ Instantiates a filter. [[ml-put-filter-desc]] ==== {api-description-title} -A {stack-ov}/ml-rules.html[filter] contains a list of strings. +A {ml-docs}/ml-rules.html[filter] contains a list of strings. It can be used by one or more jobs. Specifically, filters are referenced in the `custom_rules` property of detector configuration objects. diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index 5003f8c4f5403..76b1671fed595 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -2,7 +2,7 @@ tag::aggregations[] If set, the {dfeed} performs aggregation searches. Support for aggregations is limited and should only be used with low cardinality data. For more information, see -{stack-ov}/ml-configuring-aggregation.html[Aggregating data for faster performance]. +{ml-docs}/ml-configuring-aggregation.html[Aggregating data for faster performance]. end::aggregations[] tag::allow-lazy-open[] @@ -203,7 +203,7 @@ at the same time as `categorization_filters`. The categorization analyzer specifies how the `categorization_field` is interpreted by the categorization process. The syntax is very similar to that used to define the `analyzer` in the <>. For more information, see -{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. +{ml-docs}/ml-configuring-categories.html[Categorizing log messages]. + -- The `categorization_analyzer` field can be specified either as a string or as an @@ -234,7 +234,7 @@ set this value to `0`, no examples are stored. -- NOTE: The `categorization_examples_limit` only applies to analysis that uses categorization. For more information, see -{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. +{ml-docs}/ml-configuring-categories.html[Categorizing log messages]. -- end::categorization-examples-limit[] @@ -244,7 +244,7 @@ If this property is specified, the values of the specified field will be categorized. The resulting categories must be used in a detector by setting `by_field_name`, `over_field_name`, or `partition_field_name` to the keyword `mlcategory`. For more information, see -{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. +{ml-docs}/ml-configuring-categories.html[Categorizing log messages]. end::categorization-field-name[] tag::categorization-filters[] @@ -254,7 +254,7 @@ are used to filter out matching sequences from the categorization field values. You can use this functionality to fine tune the categorization by excluding sequences from consideration when categories are defined. For example, you can exclude SQL statements that appear in your log files. For more information, see -{stack-ov}/ml-configuring-categories.html[Categorizing log messages]. This +{ml-docs}/ml-configuring-categories.html[Categorizing log messages]. This property cannot be used at the same time as `categorization_analyzer`. If you only want to define simple regular expression filters that are applied prior to tokenization, setting this property is the easiest method. If you also want to @@ -299,7 +299,7 @@ tag::custom-rules[] An array of custom rule objects, which enable you to customize the way detectors operate. For example, a rule may dictate to the detector conditions under which results should be skipped. For more examples, see -{stack-ov}/ml-configuring-detector-custom-rules.html[Configuring detector custom rules]. +{ml-docs}/ml-configuring-detector-custom-rules.html[Customizing detectors with custom rules]. A custom rule has the following properties: + -- @@ -363,7 +363,7 @@ end::custom-rules[] tag::custom-settings[] Advanced configuration option. Contains custom meta data about the job. For example, it can contain custom URL information as shown in -{stack-ov}/ml-configuring-url.html[Adding custom URLs to {ml} results]. +{ml-docs}/ml-configuring-url.html[Adding custom URLs to {ml} results]. end::custom-settings[] tag::data-description[] @@ -503,7 +503,7 @@ an effort to determine whether any data has subsequently been added to the index If missing data is found, it is a good indication that the `query_delay` option is set too low and the data is being indexed after the {dfeed} has passed that moment in time. See -{stack-ov}/ml-delayed-data-detection.html[Working with delayed data]. +{ml-docs}/ml-delayed-data-detection.html[Working with delayed data]. This check runs only on real-time {dfeeds}. @@ -692,7 +692,7 @@ end::from[] tag::function[] The analysis function that is used. For example, `count`, `rare`, `mean`, `min`, `max`, and `sum`. For more information, see -{stack-ov}/ml-functions.html[Function reference]. +{ml-docs}/ml-functions.html[Function reference]. end::function[] tag::gamma[] @@ -979,7 +979,7 @@ tag::over-field-name[] The field used to split the data. In particular, this property is used for analyzing the splits with respect to the history of all splits. It is used for finding unusual values in the population of all splits. For more information, -see {stack-ov}/ml-configuring-pop.html[Performing population analysis]. +see {ml-docs}/ml-configuring-pop.html[Performing population analysis]. end::over-field-name[] tag::outlier-fraction[] @@ -1049,7 +1049,7 @@ tag::script-fields[] Specifies scripts that evaluate custom expressions and returns script fields to the {dfeed}. The detector configuration objects in a job can contain functions that use these script fields. For more information, see -{stack-ov}/ml-configuring-transform.html[Transforming data with script fields] +{ml-docs}/ml-configuring-transform.html[Transforming data with script fields] and <>. end::script-fields[] diff --git a/docs/reference/modules/node.asciidoc b/docs/reference/modules/node.asciidoc index 35cfd89b80dc2..a41b98b7b2078 100644 --- a/docs/reference/modules/node.asciidoc +++ b/docs/reference/modules/node.asciidoc @@ -46,7 +46,7 @@ A node that has `xpack.ml.enabled` and `node.ml` set to `true`, which is the default behavior in the {es} {default-dist}. If you want to use {ml-features}, there must be at least one {ml} node in your cluster. For more information about {ml-features}, see -{stack-ov}/xpack-ml.html[Machine learning in the {stack}]. +{ml-docs}/xpack-ml.html[Machine learning in the {stack}]. + IMPORTANT: If you use the {oss-dist}, do not set `node.ml`. Otherwise, the node fails to start. diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 57c7bf93092a7..8580705623146 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -631,19 +631,19 @@ more details. === Calendar resources See <> and -{stack-ov}/ml-calendars.html[Calendars and scheduled events]. +{ml-docs}/ml-calendars.html[Calendars and scheduled events]. [role="exclude",id="ml-filter-resource"] === Filter resources See <> and -{stack-ov}/ml-rules.html[Machine learning custom rules]. +{ml-docs}/ml-rules.html[Machine learning custom rules]. [role="exclude",id="ml-event-resource"] === Scheduled event resources See <> and -{stack-ov}/ml-calendars.html[Calendars and scheduled events]. +{ml-docs}/ml-calendars.html[Calendars and scheduled events]. [role="exclude",id="index-apis"] === Index APIs diff --git a/docs/reference/setup/restart-cluster.asciidoc b/docs/reference/setup/restart-cluster.asciidoc index 1f56e7ffa9c08..58402758a72d8 100644 --- a/docs/reference/setup/restart-cluster.asciidoc +++ b/docs/reference/setup/restart-cluster.asciidoc @@ -55,7 +55,7 @@ was automatically saved. This option avoids the overhead of managing active jobs during the shutdown and is faster than explicitly stopping {dfeeds} and closing jobs. -* {stack-ov}/stopping-ml.html[Stop all {dfeeds} and close all jobs]. This option +* {ml-docs}/stopping-ml.html[Stop all {dfeeds} and close all jobs]. This option saves the model state at the time of closure. When you reopen the jobs after the cluster restart, they use the exact same model. However, saving the latest model state takes longer than using upgrade mode, especially if you have a lot of jobs diff --git a/docs/reference/upgrade/close-ml.asciidoc b/docs/reference/upgrade/close-ml.asciidoc index 92be1e4106d87..09c6ee53b6930 100644 --- a/docs/reference/upgrade/close-ml.asciidoc +++ b/docs/reference/upgrade/close-ml.asciidoc @@ -34,7 +34,7 @@ state that was automatically saved. This option avoids the overhead of managing active jobs during the upgrade and is faster than explicitly stopping {dfeeds} and closing jobs. -* {stack-ov}/stopping-ml.html[Stop all {dfeeds} and close all jobs]. This option +* {ml-docs}/stopping-ml.html[Stop all {dfeeds} and close all jobs]. This option saves the model state at the time of closure. When you reopen the jobs after the upgrade, they use the exact same model. However, saving the latest model state takes longer than using upgrade mode, especially if you have a lot of jobs or diff --git a/docs/reference/upgrade/reindex_upgrade.asciidoc b/docs/reference/upgrade/reindex_upgrade.asciidoc index 151506797f52d..7f576bd42700e 100644 --- a/docs/reference/upgrade/reindex_upgrade.asciidoc +++ b/docs/reference/upgrade/reindex_upgrade.asciidoc @@ -62,7 +62,7 @@ If you use {ml-features} and your {ml} indices were created before {prev-major-version}, you must temporarily halt the tasks associated with your {ml} jobs and {dfeeds} and prevent new jobs from opening during the reindex. Use the <> or -{stack-ov}/stopping-ml.html[stop all {dfeeds} and close all {ml} jobs]. +{ml-docs}/stopping-ml.html[stop all {dfeeds} and close all {ml} jobs]. If you use {es} {security-features}, before you reindex `.security*` internal indices it is a good idea to create a temporary superuser account in the `file` @@ -121,7 +121,7 @@ from a 6.6 or later cluster, it is a good idea to temporarily halt the tasks associated with your {ml} jobs and {dfeeds} to prevent inconsistencies between different {ml} indices that are reindexed at slightly different times. Use the <> or -{stack-ov}/stopping-ml.html[stop all {dfeeds} and close all {ml} jobs]. +{ml-docs}/stopping-ml.html[stop all {dfeeds} and close all {ml} jobs]. endif::include-xpack[] ============================================= From 87345557de5430b819821f172b2cd20a05192a4e Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Thu, 19 Dec 2019 17:50:14 -0500 Subject: [PATCH 285/686] Handle renaming the README (#50404) We renamed README.textile to README.asciidoc but a bunch of tests and the package build itself still pointed at the old name. This switches them the new name. --- distribution/archives/build.gradle | 2 +- qa/os/bats/utils/packages.bash | 2 +- qa/os/bats/utils/tar.bash | 2 +- .../test/java/org/elasticsearch/packaging/util/Archives.java | 2 +- .../src/test/java/org/elasticsearch/packaging/util/Docker.java | 2 +- .../test/java/org/elasticsearch/packaging/util/Packages.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/distribution/archives/build.gradle b/distribution/archives/build.gradle index 0c9bbc2063839..022453f945e6a 100644 --- a/distribution/archives/build.gradle +++ b/distribution/archives/build.gradle @@ -77,7 +77,7 @@ CopySpec archiveFiles(CopySpec modulesFiles, String distributionType, String pla } } from(rootProject.projectDir) { - include 'README.textile' + include 'README.asciidoc' } from(rootProject.file('licenses')) { include oss ? 'APACHE-LICENSE-2.0.txt' : 'ELASTIC-LICENSE.txt' diff --git a/qa/os/bats/utils/packages.bash b/qa/os/bats/utils/packages.bash index f0c6c963fed74..87b2943a8757b 100644 --- a/qa/os/bats/utils/packages.bash +++ b/qa/os/bats/utils/packages.bash @@ -136,7 +136,7 @@ verify_package_installation() { assert_file "$ESPLUGINS" d root root 755 assert_file "$ESMODULES" d root root 755 assert_file "$ESHOME/NOTICE.txt" f root root 644 - assert_file "$ESHOME/README.textile" f root root 644 + assert_file "$ESHOME/README.asciidoc" f root root 644 if is_dpkg; then # Env file diff --git a/qa/os/bats/utils/tar.bash b/qa/os/bats/utils/tar.bash index 142215a703682..415e79cac3802 100644 --- a/qa/os/bats/utils/tar.bash +++ b/qa/os/bats/utils/tar.bash @@ -105,6 +105,6 @@ verify_archive_installation() { assert_file "$ESHOME/logs" d elasticsearch elasticsearch 755 assert_file "$ESHOME/NOTICE.txt" f elasticsearch elasticsearch 644 assert_file "$ESHOME/LICENSE.txt" f elasticsearch elasticsearch 644 - assert_file "$ESHOME/README.textile" f elasticsearch elasticsearch 644 + assert_file "$ESHOME/README.asciidoc" f elasticsearch elasticsearch 644 assert_file_not_exist "$ESCONFIG/elasticsearch.keystore" } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java index 28234bd118701..83ff3fec8fa26 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java @@ -204,7 +204,7 @@ private static void verifyOssInstallation(Installation es, Distribution distribu Stream.of( "NOTICE.txt", "LICENSE.txt", - "README.textile" + "README.asciidoc" ).forEach(doc -> assertThat(es.home.resolve(doc), file(File, owner, owner, p644))); } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java index 313060cdd3989..ef3382cd86eff 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java @@ -445,7 +445,7 @@ private static void verifyOssInstallation(Installation es) { "elasticsearch-shard" ).forEach(executable -> assertPermissionsAndOwnership(es.bin(executable), p755)); - Stream.of("LICENSE.txt", "NOTICE.txt", "README.textile").forEach(doc -> assertPermissionsAndOwnership(es.home.resolve(doc), p644)); + Stream.of("LICENSE.txt", "NOTICE.txt", "README.asciidoc").forEach(doc -> assertPermissionsAndOwnership(es.home.resolve(doc), p644)); // These are installed to help users who are working with certificates. Stream.of("zip", "unzip").forEach(cliPackage -> { diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Packages.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Packages.java index 19521e6b919d0..036358696d21d 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Packages.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Packages.java @@ -207,7 +207,7 @@ private static void verifyOssInstallation(Installation es, Distribution distribu Stream.of( "NOTICE.txt", - "README.textile" + "README.asciidoc" ).forEach(doc -> assertThat(es.home.resolve(doc), file(File, "root", "root", p644))); assertThat(es.envFile, file(File, "root", "elasticsearch", p660)); From b6fdfb750fb39df8de3db0d46c5539245cf26daf Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Thu, 19 Dec 2019 16:16:57 -0700 Subject: [PATCH 286/686] [DOCS] Deterministic scripted queries are cached (#50408) Refs: #49321 --- docs/reference/modules/indices/request_cache.asciidoc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/reference/modules/indices/request_cache.asciidoc b/docs/reference/modules/indices/request_cache.asciidoc index 11c8180eb4739..45d46e3a7714d 100644 --- a/docs/reference/modules/indices/request_cache.asciidoc +++ b/docs/reference/modules/indices/request_cache.asciidoc @@ -21,6 +21,9 @@ but it will cache `hits.total`, <>, and <>. Most queries that use `now` (see <>) cannot be cached. + +Scripted queries that use the API calls which are non-deterministic, such as +`Math.random()` or `new Date()` are not cached. =================================== [float] @@ -95,10 +98,6 @@ GET /my_index/_search?request_cache=true ----------------------------- // TEST[continued] -IMPORTANT: If your query uses a script whose result is not deterministic (e.g. -it uses a random function or references the current time) you should set the -`request_cache` flag to `false` to disable caching for that request. - Requests where `size` is greater than 0 will not be cached even if the request cache is enabled in the index settings. To cache these requests you will need to use the query-string parameter detailed here. From 1a21b5b050905f1e930ee77932f1a11067386500 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Thu, 19 Dec 2019 18:55:58 -0500 Subject: [PATCH 287/686] Docs: Add an example to CONTRIBUTING.md (#50398) Adds an example of accessing Elasticsearch after building it from source for the first time to CONTRIBUTING.md. I'm sure how to do it is intuitive to some folks but I had to read three two build.gradle file to find the password and I'd like to save other new folks the trouble. --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57021e3be565d..dd3bbfb1bf5e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,6 +119,10 @@ For IntelliJ, the minimum version that we support is [IntelliJ 2017.2][intellij] ./gradlew :run +You can access Elasticsearch with: + + curl -u elastic:password localhost:9200 + ### Configuring IDEs And Running Tests Eclipse users can automatically configure their IDE: `./gradlew eclipse` From 592bb71233cfef001ab7e6226f9073c5b1d581e0 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 19 Dec 2019 23:49:15 -0500 Subject: [PATCH 288/686] Mute testHistoryIsWrittenWithSuccess Tracked at #50353 --- .../elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 7192febb6b11e..347e1fac4f96a 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -1005,6 +1005,7 @@ public void testILMRolloverOnManuallyRolledIndex() throws Exception { assertBusy(() -> assertTrue(indexExists(thirdIndex))); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50353") public void testHistoryIsWrittenWithSuccess() throws Exception { String index = "index"; From 46179ebfaa736e32e2a07d6f3e0252f68b4f812a Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 20 Dec 2019 00:34:57 -0500 Subject: [PATCH 289/686] Mute testHistoryIsWrittenWithDeletion Tracked at #50353 --- .../elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 347e1fac4f96a..3392ae4954353 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -1077,6 +1077,7 @@ public void testHistoryIsWrittenWithFailure() throws Exception { assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", false, "ERROR"), 30, TimeUnit.SECONDS); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50353") public void testHistoryIsWrittenWithDeletion() throws Exception { String index = "index"; From f69fd9697ce736e06c2539dada39e44b4947f057 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 20 Dec 2019 00:39:53 -0500 Subject: [PATCH 290/686] Use peer recovery retention leases for indices without soft-deletes (#50351) Today, the replica allocator uses peer recovery retention leases to select the best-matched copies when allocating replicas of indices with soft-deletes. We can employ this mechanism for indices without soft-deletes because the retaining sequence number of a PRRL is the persisted global checkpoint (plus one) of that copy. If the primary and replica have the same retaining sequence number, then we should be able to perform a noop recovery. The reason is that we must be retaining translog up to the local checkpoint of the safe commit, which is at most the global checkpoint of either copy). The only limitation is that we might not cancel ongoing file-based recoveries with PRRLs for noop recoveries. We can't make the translog retention policy comply with PRRLs. We also have this problem with soft-deletes if a PRRL is about to expire. Relates #45136 Relates #46959 --- .../upgrades/FullClusterRestartIT.java | 4 +- .../elasticsearch/upgrades/RecoveryIT.java | 5 +- .../org/elasticsearch/index/IndexService.java | 4 +- .../index/seqno/ReplicationTracker.java | 17 +++-- .../elasticsearch/index/shard/IndexShard.java | 9 ++- .../recovery/RecoverySourceHandler.java | 62 ++++++++----------- .../gateway/ReplicaShardAllocatorIT.java | 8 +-- .../index/seqno/RetentionLeaseIT.java | 30 --------- .../shard/IndexShardRetentionLeaseTests.java | 3 +- .../recovery/RecoverySourceHandlerTests.java | 9 +-- .../test/rest/ESRestTestCase.java | 18 +++++- 11 files changed, 69 insertions(+), 100 deletions(-) diff --git a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java index 2d367261f8895..67d3007e9af16 100644 --- a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java +++ b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java @@ -1278,7 +1278,7 @@ public void testOperationBasedRecovery() throws Exception { } } flush(index, true); - ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index); + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index, false); // less than 10% of the committed docs (see IndexSetting#FILE_BASED_RECOVERY_THRESHOLD_SETTING). int uncommittedDocs = randomIntBetween(0, (int) (committedDocs * 0.1)); for (int i = 0; i < uncommittedDocs; i++) { @@ -1288,6 +1288,7 @@ public void testOperationBasedRecovery() throws Exception { } else { ensureGreen(index); assertNoFileBasedRecovery(index, n -> true); + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index, true); } } @@ -1312,6 +1313,7 @@ public void testTurnOffTranslogRetentionAfterUpgraded() throws Exception { ensureGreen(index); flush(index, true); assertEmptyTranslog(index); + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index, true); } } } diff --git a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java index cd4a07aab3ec0..7bd52d266914d 100644 --- a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java +++ b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java @@ -695,7 +695,7 @@ public void testOperationBasedRecovery() throws Exception { ensureGreen(index); indexDocs(index, 0, randomIntBetween(100, 200)); flush(index, randomBoolean()); - ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index); + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index, false); // uncommitted docs must be less than 10% of committed docs (see IndexSetting#FILE_BASED_RECOVERY_THRESHOLD_SETTING). indexDocs(index, randomIntBetween(0, 100), randomIntBetween(0, 3)); } else { @@ -705,6 +705,9 @@ public void testOperationBasedRecovery() throws Exception { || nodeName.startsWith(CLUSTER_NAME + "-0") || (nodeName.startsWith(CLUSTER_NAME + "-1") && Booleans.parseBoolean(System.getProperty("tests.first_round")) == false)); indexDocs(index, randomIntBetween(0, 100), randomIntBetween(0, 3)); + if (CLUSTER_TYPE == ClusterType.UPGRADED) { + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index, true); + } } } diff --git a/server/src/main/java/org/elasticsearch/index/IndexService.java b/server/src/main/java/org/elasticsearch/index/IndexService.java index 481ad3f5a7f6f..15fd0de2760a6 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexService.java +++ b/server/src/main/java/org/elasticsearch/index/IndexService.java @@ -820,9 +820,7 @@ private void maybeSyncGlobalCheckpoints() { } private void syncRetentionLeases() { - if (indexSettings.isSoftDeleteEnabled()) { - sync(IndexShard::syncRetentionLeases, "retention lease"); - } + sync(IndexShard::syncRetentionLeases, "retention lease"); } private void sync(final Consumer sync, final String source) { diff --git a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java index 8c42784b88f82..bfe89a65d89df 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java @@ -895,10 +895,12 @@ public ReplicationTracker( this.pendingInSync = new HashSet<>(); this.routingTable = null; this.replicationGroup = null; - this.hasAllPeerRecoveryRetentionLeases = indexSettings.isSoftDeleteEnabled() && - (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_6_0) || - (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_4_0) && - indexSettings.getIndexMetaData().getState() == IndexMetaData.State.OPEN)); + this.hasAllPeerRecoveryRetentionLeases = indexSettings.getIndexVersionCreated().onOrAfter(Version.V_8_0_0) + || (indexSettings.isSoftDeleteEnabled() && + (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_6_0) || + (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_4_0) && + indexSettings.getIndexMetaData().getState() == IndexMetaData.State.OPEN))); + this.fileBasedRecoveryThreshold = IndexSettings.FILE_BASED_RECOVERY_THRESHOLD_SETTING.get(indexSettings.getSettings()); this.safeCommitInfoSupplier = safeCommitInfoSupplier; assert Version.V_EMPTY.equals(indexSettings.getIndexVersionCreated()) == false; @@ -994,10 +996,7 @@ public synchronized void activatePrimaryMode(final long localCheckpoint) { updateLocalCheckpoint(shardAllocationId, checkpoints.get(shardAllocationId), localCheckpoint); updateGlobalCheckpointOnPrimary(); - if (indexSettings.isSoftDeleteEnabled()) { - addPeerRecoveryRetentionLeaseForSolePrimary(); - } - + addPeerRecoveryRetentionLeaseForSolePrimary(); assert invariant(); } @@ -1358,7 +1357,7 @@ public synchronized boolean hasAllPeerRecoveryRetentionLeases() { * prior to {@link Version#V_7_4_0} that does not create peer-recovery retention leases. */ public synchronized void createMissingPeerRecoveryRetentionLeases(ActionListener listener) { - if (indexSettings().isSoftDeleteEnabled() && hasAllPeerRecoveryRetentionLeases == false) { + if (hasAllPeerRecoveryRetentionLeases == false) { final List shardRoutings = routingTable.assignedShards(); final GroupedActionListener groupedActionListener = new GroupedActionListener<>(ActionListener.wrap(vs -> { setHasAllPeerRecoveryRetentionLeases(); diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index 059961e1e5897..5415a433d8670 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -1892,10 +1892,10 @@ boolean shouldRollTranslogGeneration() { public void onSettingsChanged() { Engine engineOrNull = getEngineOrNull(); if (engineOrNull != null) { - final boolean useRetentionLeasesInPeerRecovery = this.useRetentionLeasesInPeerRecovery; + final boolean disableTranslogRetention = indexSettings.isSoftDeleteEnabled() && useRetentionLeasesInPeerRecovery; engineOrNull.onSettingsChanged( - useRetentionLeasesInPeerRecovery ? TimeValue.MINUS_ONE : indexSettings.getTranslogRetentionAge(), - useRetentionLeasesInPeerRecovery ? new ByteSizeValue(-1) : indexSettings.getTranslogRetentionSize(), + disableTranslogRetention ? TimeValue.MINUS_ONE : indexSettings.getTranslogRetentionAge(), + disableTranslogRetention ? new ByteSizeValue(-1) : indexSettings.getTranslogRetentionSize(), indexSettings.getSoftDeleteRetentionOperations() ); } @@ -2224,7 +2224,6 @@ public boolean assertRetentionLeasesPersisted() throws IOException { public void syncRetentionLeases() { assert assertPrimaryMode(); verifyNotClosed(); - ensureSoftDeletesEnabled("retention leases"); replicationTracker.renewPeerRecoveryRetentionLeases(); final Tuple retentionLeases = getRetentionLeases(true); if (retentionLeases.v1()) { @@ -2619,7 +2618,7 @@ public RetentionLease addPeerRecoveryRetentionLease(String nodeId, long globalCh ActionListener listener) { assert assertPrimaryMode(); // only needed for BWC reasons involving rolling upgrades from versions that do not support PRRLs: - assert indexSettings.getIndexVersionCreated().before(Version.V_7_4_0); + assert indexSettings.getIndexVersionCreated().before(Version.V_7_4_0) || indexSettings.isSoftDeleteEnabled() == false; return replicationTracker.addPeerRecoveryRetentionLease(nodeId, globalCheckpoint, listener); } diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java index 285edc329be06..07db659299e7c 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java @@ -165,12 +165,12 @@ public void recoverToTarget(ActionListener listener) { throw new DelayRecoveryException("source node does not have the shard listed in its state as allocated on the node"); } assert targetShardRouting.initializing() : "expected recovery target to be initializing but was " + targetShardRouting; - retentionLeaseRef.set(softDeletesEnabled ? shard.getRetentionLeases().get( - ReplicationTracker.getPeerRecoveryRetentionLeaseId(targetShardRouting)) : null); + retentionLeaseRef.set( + shard.getRetentionLeases().get(ReplicationTracker.getPeerRecoveryRetentionLeaseId(targetShardRouting))); }, shardId + " validating recovery target ["+ request.targetAllocationId() + "] registered ", shard, cancellableThreads, logger); final Engine.HistorySource historySource; - if (shard.useRetentionLeasesInPeerRecovery() || retentionLeaseRef.get() != null) { + if (softDeletesEnabled && (shard.useRetentionLeasesInPeerRecovery() || retentionLeaseRef.get() != null)) { historySource = Engine.HistorySource.INDEX; } else { historySource = Engine.HistorySource.TRANSLOG; @@ -190,7 +190,7 @@ && isTargetSameHistory() // Also it's pretty cheap when soft deletes are enabled, and it'd be a disaster if we tried a sequence-number-based recovery // without having a complete history. - if (isSequenceNumberBasedRecovery && retentionLeaseRef.get() != null) { + if (isSequenceNumberBasedRecovery && softDeletesEnabled && retentionLeaseRef.get() != null) { // all the history we need is retained by an existing retention lease, so we do not need a separate retention lock retentionLock.close(); logger.trace("history is retained by {}", retentionLeaseRef.get()); @@ -209,7 +209,7 @@ && isTargetSameHistory() if (isSequenceNumberBasedRecovery) { logger.trace("performing sequence numbers based recovery. starting at [{}]", request.startingSeqNo()); startingSeqNo = request.startingSeqNo(); - if (softDeletesEnabled && retentionLeaseRef.get() == null) { + if (retentionLeaseRef.get() == null) { createRetentionLease(startingSeqNo, ActionListener.map(sendFileStep, ignored -> SendFileResult.EMPTY)); } else { sendFileStep.onResponse(SendFileResult.EMPTY); @@ -251,36 +251,24 @@ && isTargetSameHistory() }); final StepListener deleteRetentionLeaseStep = new StepListener<>(); - if (softDeletesEnabled) { - runUnderPrimaryPermit(() -> { - try { - // If the target previously had a copy of this shard then a file-based recovery might move its global - // checkpoint backwards. We must therefore remove any existing retention lease so that we can create a - // new one later on in the recovery. - shard.removePeerRecoveryRetentionLease(request.targetNode().getId(), - new ThreadedActionListener<>(logger, shard.getThreadPool(), ThreadPool.Names.GENERIC, - deleteRetentionLeaseStep, false)); - } catch (RetentionLeaseNotFoundException e) { - logger.debug("no peer-recovery retention lease for " + request.targetAllocationId()); - deleteRetentionLeaseStep.onResponse(null); - } - }, shardId + " removing retention leaes for [" + request.targetAllocationId() + "]", - shard, cancellableThreads, logger); - } else { - deleteRetentionLeaseStep.onResponse(null); - } + runUnderPrimaryPermit(() -> { + try { + // If the target previously had a copy of this shard then a file-based recovery might move its global + // checkpoint backwards. We must therefore remove any existing retention lease so that we can create a + // new one later on in the recovery. + shard.removePeerRecoveryRetentionLease(request.targetNode().getId(), + new ThreadedActionListener<>(logger, shard.getThreadPool(), ThreadPool.Names.GENERIC, + deleteRetentionLeaseStep, false)); + } catch (RetentionLeaseNotFoundException e) { + logger.debug("no peer-recovery retention lease for " + request.targetAllocationId()); + deleteRetentionLeaseStep.onResponse(null); + } + }, shardId + " removing retention lease for [" + request.targetAllocationId() + "]", + shard, cancellableThreads, logger); deleteRetentionLeaseStep.whenComplete(ignored -> { assert Transports.assertNotTransportThread(RecoverySourceHandler.this + "[phase1]"); - - final Consumer> createRetentionLeaseAsync; - if (softDeletesEnabled) { - createRetentionLeaseAsync = l -> createRetentionLease(startingSeqNo, l); - } else { - createRetentionLeaseAsync = l -> l.onResponse(null); - } - - phase1(safeCommitRef.getIndexCommit(), createRetentionLeaseAsync, () -> estimateNumOps, sendFileStep); + phase1(safeCommitRef.getIndexCommit(), startingSeqNo, () -> estimateNumOps, sendFileStep); }, onFailure); } catch (final Exception e) { @@ -451,8 +439,7 @@ static final class SendFileResult { * segments that are missing. Only segments that have the same size and * checksum can be reused */ - void phase1(IndexCommit snapshot, Consumer> createRetentionLease, - IntSupplier translogOps, ActionListener listener) { + void phase1(IndexCommit snapshot, long startingSeqNo, IntSupplier translogOps, ActionListener listener) { cancellableThreads.checkForCancel(); final Store store = shard.store(); try { @@ -526,7 +513,7 @@ void phase1(IndexCommit snapshot, Consumer> creat sendFileInfoStep.whenComplete(r -> sendFiles(store, phase1Files.toArray(new StoreFileMetaData[0]), translogOps, sendFilesStep), listener::onFailure); - sendFilesStep.whenComplete(r -> createRetentionLease.accept(createRetentionLeaseStep), listener::onFailure); + sendFilesStep.whenComplete(r -> createRetentionLease(startingSeqNo, createRetentionLeaseStep), listener::onFailure); createRetentionLeaseStep.whenComplete(retentionLease -> { @@ -554,7 +541,7 @@ void phase1(IndexCommit snapshot, Consumer> creat // but we must still create a retention lease final StepListener createRetentionLeaseStep = new StepListener<>(); - createRetentionLease.accept(createRetentionLeaseStep); + createRetentionLease(startingSeqNo, createRetentionLeaseStep); createRetentionLeaseStep.whenComplete(retentionLease -> { final TimeValue took = stopWatch.totalTime(); logger.trace("recovery [phase1]: took [{}]", took); @@ -590,7 +577,8 @@ private void createRetentionLease(final long startingSeqNo, ActionListener addRetentionLeaseStep = new StepListener<>(); final long estimatedGlobalCheckpoint = startingSeqNo - 1; final RetentionLease newLease = shard.addPeerRecoveryRetentionLease(request.targetNode().getId(), diff --git a/server/src/test/java/org/elasticsearch/gateway/ReplicaShardAllocatorIT.java b/server/src/test/java/org/elasticsearch/gateway/ReplicaShardAllocatorIT.java index 35064b0063676..9868adfe3b86b 100644 --- a/server/src/test/java/org/elasticsearch/gateway/ReplicaShardAllocatorIT.java +++ b/server/src/test/java/org/elasticsearch/gateway/ReplicaShardAllocatorIT.java @@ -78,7 +78,7 @@ public void testPreferCopyCanPerformNoopRecovery() throws Exception { assertAcked( client().admin().indices().prepareCreate(indexName) .setSettings(Settings.builder() - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1) .put(IndexSettings.FILE_BASED_RECOVERY_THRESHOLD_SETTING.getKey(), 1.0f) @@ -211,7 +211,7 @@ public void testFullClusterRestartPerformNoopRecovery() throws Exception { assertAcked( client().admin().indices().prepareCreate(indexName) .setSettings(Settings.builder() - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexSettings.INDEX_TRANSLOG_FLUSH_THRESHOLD_SIZE_SETTING.getKey(), randomIntBetween(10, 100) + "kb") .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, numOfReplicas) @@ -248,7 +248,7 @@ public void testPreferCopyWithHighestMatchingOperations() throws Exception { assertAcked( client().admin().indices().prepareCreate(indexName) .setSettings(Settings.builder() - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexSettings.INDEX_TRANSLOG_FLUSH_THRESHOLD_SIZE_SETTING.getKey(), randomIntBetween(10, 100) + "kb") .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1) @@ -329,7 +329,7 @@ public void testPeerRecoveryForClosedIndices() throws Exception { createIndex(indexName, Settings.builder() .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) .put(IndexService.GLOBAL_CHECKPOINT_SYNC_INTERVAL_SETTING.getKey(), "100ms") .put(IndexService.RETENTION_LEASE_SYNC_INTERVAL_SETTING.getKey(), "100ms") .build()); diff --git a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseIT.java b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseIT.java index 831422f8dad86..f081f87eaa365 100644 --- a/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseIT.java +++ b/server/src/test/java/org/elasticsearch/index/seqno/RetentionLeaseIT.java @@ -336,36 +336,6 @@ public void testBackgroundRetentionLeaseSync() throws Exception { } } - public void testRetentionLeasesBackgroundSyncWithSoftDeletesDisabled() throws Exception { - final int numberOfReplicas = 2 - scaledRandomIntBetween(0, 2); - internalCluster().ensureAtLeastNumDataNodes(1 + numberOfReplicas); - TimeValue syncIntervalSetting = TimeValue.timeValueMillis(between(1, 100)); - final Settings settings = Settings.builder() - .put("index.number_of_shards", 1) - .put("index.number_of_replicas", numberOfReplicas) - .put(IndexService.RETENTION_LEASE_SYNC_INTERVAL_SETTING.getKey(), syncIntervalSetting.getStringRep()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false) - .build(); - createIndex("index", settings); - final String primaryShardNodeId = clusterService().state().routingTable().index("index").shard(0).primaryShard().currentNodeId(); - final String primaryShardNodeName = clusterService().state().nodes().get(primaryShardNodeId).getName(); - final MockTransportService primaryTransportService = (MockTransportService) internalCluster().getInstance( - TransportService.class, primaryShardNodeName); - final AtomicBoolean backgroundSyncRequestSent = new AtomicBoolean(); - primaryTransportService.addSendBehavior((connection, requestId, action, request, options) -> { - if (action.startsWith(RetentionLeaseBackgroundSyncAction.ACTION_NAME)) { - backgroundSyncRequestSent.set(true); - } - connection.sendRequest(requestId, action, request, options); - }); - final long start = System.nanoTime(); - ensureGreen("index"); - final long syncEnd = System.nanoTime(); - // We sleep long enough for the retention leases background sync to be triggered - Thread.sleep(Math.max(0, randomIntBetween(2, 3) * syncIntervalSetting.millis() - TimeUnit.NANOSECONDS.toMillis(syncEnd - start))); - assertFalse("retention leases background sync must be a noop if soft deletes is disabled", backgroundSyncRequestSent.get()); - } - public void testRetentionLeasesSyncOnRecovery() throws Exception { final int numberOfReplicas = 2 - scaledRandomIntBetween(0, 2); internalCluster().ensureAtLeastNumDataNodes(1 + numberOfReplicas); diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardRetentionLeaseTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardRetentionLeaseTests.java index ed429bb680d7d..31bdfce261ad9 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardRetentionLeaseTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardRetentionLeaseTests.java @@ -314,8 +314,7 @@ public void testRetentionLeasesActionsFailWithSoftDeletesDisabled() throws Excep assertThat(expectThrows(AssertionError.class, () -> shard.removeRetentionLease( randomAlphaOfLength(10), ActionListener.wrap(() -> {}))).getMessage(), equalTo("retention leases requires soft deletes but [index] does not have soft deletes enabled")); - assertThat(expectThrows(AssertionError.class, shard::syncRetentionLeases).getMessage(), - equalTo("retention leases requires soft deletes but [index] does not have soft deletes enabled")); + shard.syncRetentionLeases(); closeShards(shard); } diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java index e57f5162a7a49..db9f52f942e63 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java @@ -64,7 +64,6 @@ import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.seqno.ReplicationTracker; -import org.elasticsearch.index.seqno.RetentionLease; import org.elasticsearch.index.seqno.RetentionLeases; import org.elasticsearch.index.seqno.SeqNoStats; import org.elasticsearch.index.seqno.SequenceNumbers; @@ -102,7 +101,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; import java.util.function.IntSupplier; import java.util.zip.CRC32; @@ -467,10 +465,9 @@ public void testThrowExceptionOnPrimaryRelocatedBeforePhase1Started() throws IOE between(1, 8)) { @Override - void phase1(IndexCommit snapshot, Consumer> createRetentionLease, - IntSupplier translogOps, ActionListener listener) { + void phase1(IndexCommit snapshot, long startingSeqNo, IntSupplier translogOps, ActionListener listener) { phase1Called.set(true); - super.phase1(snapshot, createRetentionLease, translogOps, listener); + super.phase1(snapshot, startingSeqNo, translogOps, listener); } @Override @@ -686,7 +683,7 @@ public void cleanFiles(int totalTranslogOps, long globalCheckpoint, Store.Metada try { final CountDownLatch latch = new CountDownLatch(1); handler.phase1(DirectoryReader.listCommits(dir).get(0), - l -> recoveryExecutor.execute(() -> l.onResponse(null)), + 0, () -> 0, new LatchedActionListener<>(phase1Listener, latch)); latch.await(); diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index 73ca1dde99ca8..f10f444e918ed 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -52,6 +52,7 @@ import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.snapshots.SnapshotState; import org.elasticsearch.test.ESTestCase; @@ -87,12 +88,15 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Collectors; import static java.util.Collections.sort; import static java.util.Collections.unmodifiableList; import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.in; /** * Superclass for tests that interact with an external test cluster using Elasticsearch's {@link RestClient}. @@ -1128,7 +1132,7 @@ public void assertEmptyTranslog(String index) throws Exception { * Peer recovery retention leases are renewed and synced to replicas periodically (every 30 seconds). This ensures * that we have renewed every PRRL to the global checkpoint of the corresponding copy and properly synced to all copies. */ - public void ensurePeerRecoveryRetentionLeasesRenewedAndSynced(String index) throws Exception { + public void ensurePeerRecoveryRetentionLeasesRenewedAndSynced(String index, boolean alwaysExists) throws Exception { assertBusy(() -> { Map stats = entityAsMap(client().performRequest(new Request("GET", index + "/_stats?level=shards"))); @SuppressWarnings("unchecked") Map>> shards = @@ -1139,14 +1143,24 @@ public void ensurePeerRecoveryRetentionLeasesRenewedAndSynced(String index) thro assertNotNull(globalCheckpoint); @SuppressWarnings("unchecked") List> retentionLeases = (List>) XContentMapValues.extractValue("retention_leases.leases", copy); - if (retentionLeases == null) { + if (alwaysExists == false && retentionLeases == null) { continue; } + assertNotNull(retentionLeases); for (Map retentionLease : retentionLeases) { if (((String) retentionLease.get("id")).startsWith("peer_recovery/")) { assertThat(retentionLease.get("retaining_seq_no"), equalTo(globalCheckpoint + 1)); } } + if (alwaysExists) { + List existingLeaseIds = retentionLeases.stream().map(lease -> (String) lease.get("id")) + .collect(Collectors.toList()); + List expectedLeaseIds = shard.stream() + .map(shr -> (String) XContentMapValues.extractValue("routing.node", shr)) + .map(ReplicationTracker::getPeerRecoveryRetentionLeaseId) + .collect(Collectors.toList()); + assertThat("not every active copy has established its PPRL", expectedLeaseIds, everyItem(in(existingLeaseIds))); + } } } }, 60, TimeUnit.SECONDS); From 4bd1a35d4198a1fc029f65481739f23a6972187f Mon Sep 17 00:00:00 2001 From: Sivagurunathan Velayutham Date: Thu, 19 Dec 2019 22:41:19 -0800 Subject: [PATCH 291/686] Adding tests for review --- .../xpack/core/ilm/ForceMergeAction.java | 4 ++-- .../xpack/core/ilm/ForceMergeActionTests.java | 4 +++- .../xpack/ilm/TimeSeriesLifecycleActionsIT.java | 11 +++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java index 6ee7c3eb2df19..c3c0288c9b315 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java @@ -61,8 +61,8 @@ public ForceMergeAction(int maxNumSegments, @Nullable Codec codec) { + "] must be a positive integer"); } this.maxNumSegments = maxNumSegments; - if (codec != null && Codec.forName(codec.getName()) == null) { - throw new IllegalArgumentException("Compression type of " + codec.getName() + "does not exist"); + if (codec != null && CodecService.BEST_COMPRESSION_CODEC.equals(codec.getName()) == false) { + throw new IllegalArgumentException("Compression type not found instead " + codec.getName() + " is used."); } this.codec = codec; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index 042c12b9fcb78..a81a6e1350da6 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -118,7 +118,9 @@ public void testInvalidNegativeSegmentNumber() { } public void testInvalidCodec() { - + Exception r = expectThrows(IllegalArgumentException.class, () -> new + ForceMergeAction(randomIntBetween(-10, 0), Codec.forName(CodecService.DEFAULT_CODEC))); + assertThat(r.getMessage(), equalTo("Best Compression type not found instead " + CodecService.DEFAULT_CODEC + " is used.")); } public void testToSteps() { diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 3392ae4954353..c40e96bcdd9e8 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -25,6 +25,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.codec.CodecService; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xpack.core.ilm.AllocateAction; import org.elasticsearch.xpack.core.ilm.DeleteAction; @@ -391,7 +392,7 @@ public void testReadOnly() throws Exception { } @SuppressWarnings("unchecked") - public void testForceMergeAction() throws Exception { + public void forceMergeActionWithCodec(Codec codec) throws Exception { createIndexWithSettings(index, Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)); for (int i = 0; i < randomIntBetween(2, 10); i++) { @@ -415,7 +416,7 @@ public void testForceMergeAction() throws Exception { }; assertThat(numSegments.get(), greaterThan(1)); - createNewSingletonPolicy("warm", new ForceMergeAction(1, Codec.getDefault())); + createNewSingletonPolicy("warm", new ForceMergeAction(1, codec)); updatePolicy(index, policy); assertBusy(() -> { @@ -427,6 +428,12 @@ public void testForceMergeAction() throws Exception { expectThrows(ResponseException.class, this::indexDocument); } + public void testForceMergeAction() throws Exception { + forceMergeActionWithCodec(null); + forceMergeActionWithCodec(Codec.forName(CodecService.BEST_COMPRESSION_CODEC)); + } + + public void testShrinkAction() throws Exception { int numShards = 6; int divisor = randomFrom(2, 3, 6); From 9d52c2ab11d0674a3ed2cc8ba7c4f9c2d34573cc Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Fri, 20 Dec 2019 08:27:33 +0100 Subject: [PATCH 292/686] [Transform] refactor source and dest validation to support CCS (#50018) refactors source and dest validation, adds support for CCS, makes resolve work like reindex/search, allow aliased dest index with a single write index. fixes #49988 fixes #49851 relates #43201 --- .../metadata/IndexNameExpressionResolver.java | 23 +- .../license/RemoteClusterLicenseChecker.java | 5 +- .../license/XPackLicenseState.java | 5 + .../validation/SourceDestValidator.java | 460 ++++++++++ .../core/transform/TransformMessages.java | 3 - .../action/PreviewTransformAction.java | 28 +- .../transform/action/PutTransformAction.java | 40 +- .../action/UpdateTransformAction.java | 29 +- .../validation/SourceDestValidatorTests.java | 822 ++++++++++++++++++ .../test/transform/preview_transforms.yml | 2 +- .../test/transform/transforms_crud.yml | 8 +- .../test/transform/transforms_update.yml | 10 +- .../TransportPreviewTransformAction.java | 279 +++--- .../action/TransportPutTransformAction.java | 56 +- .../action/TransportStartTransformAction.java | 72 +- .../TransportUpdateTransformAction.java | 38 +- ...sportPreviewTransformActionDeprecated.java | 28 +- ...ansportStartTransformActionDeprecated.java | 7 +- .../transforms/SourceDestValidator.java | 159 ---- .../transforms/SourceDestValidatorTests.java | 160 ---- 20 files changed, 1667 insertions(+), 567 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/validation/SourceDestValidator.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/common/validation/SourceDestValidatorTests.java delete mode 100644 x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/SourceDestValidator.java delete mode 100644 x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/SourceDestValidatorTests.java diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index 154b2ac23c216..93e8b84936133 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -296,10 +296,27 @@ public Index concreteWriteIndex(ClusterState state, IndicesRequest request) { if (request.indices() == null || (request.indices() != null && request.indices().length != 1)) { throw new IllegalArgumentException("indices request must specify a single index expression"); } - Context context = new Context(state, request.indicesOptions(), false, true); - Index[] indices = concreteIndices(context, request.indices()[0]); + return concreteWriteIndex(state, request.indicesOptions(), request.indices()[0], false); + } + + /** + * Utility method that allows to resolve an index expression to its corresponding single write index. + * + * @param state the cluster state containing all the data to resolve to expression to a concrete index + * @param options defines how the aliases or indices need to be resolved to concrete indices + * @param index index that can be resolved to alias or index name. + * @param allowNoIndices whether to allow resolve to no index + * @throws IllegalArgumentException if the index resolution does not lead to an index, or leads to more than one index + * @return the write index obtained as a result of the index resolution or null if no index + */ + public Index concreteWriteIndex(ClusterState state, IndicesOptions options, String index, boolean allowNoIndices) { + Context context = new Context(state, options, false, true); + Index[] indices = concreteIndices(context, index); + if (allowNoIndices && indices.length == 0) { + return null; + } if (indices.length != 1) { - throw new IllegalArgumentException("The index expression [" + request.indices()[0] + + throw new IllegalArgumentException("The index expression [" + index + "] and options provided did not point to a single write-index"); } return indices[0]; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java index 5de1186767f4b..fc7d44c191601 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java @@ -19,6 +19,7 @@ import org.elasticsearch.transport.RemoteClusterAware; import org.elasticsearch.xpack.core.action.XPackInfoAction; +import java.util.Collection; import java.util.EnumSet; import java.util.Iterator; import java.util.List; @@ -229,7 +230,7 @@ public static boolean isRemoteIndex(final String index) { * @param indices the collection of index names * @return true if the collection of index names contains a name that represents a remote index, otherwise false */ - public static boolean containsRemoteIndex(final List indices) { + public static boolean containsRemoteIndex(final Collection indices) { return indices.stream().anyMatch(RemoteClusterLicenseChecker::isRemoteIndex); } @@ -240,7 +241,7 @@ public static boolean containsRemoteIndex(final List indices) { * @param indices the collection of index names * @return list of index names that represent remote index names */ - public static List remoteIndices(final List indices) { + public static List remoteIndices(final Collection indices) { return indices.stream().filter(RemoteClusterLicenseChecker::isRemoteIndex).collect(Collectors.toList()); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java index 69ff6dddcbe48..3c4c6dc0d6777 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java @@ -611,6 +611,11 @@ public synchronized boolean isTransformAllowed() { return status.active; } + public static boolean isTransformAllowedForOperationMode(final OperationMode operationMode) { + // any license (basic and upwards) + return operationMode != License.OperationMode.MISSING; + } + /** * Rollup is always available as long as there is a valid license * diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/validation/SourceDestValidator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/validation/SourceDestValidator.java new file mode 100644 index 0000000000000..ef28fed54cf48 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/validation/SourceDestValidator.java @@ -0,0 +1,460 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.common.validation; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.ValidationException; +import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.indices.InvalidIndexNameException; +import org.elasticsearch.license.RemoteClusterLicenseChecker; +import org.elasticsearch.protocol.xpack.license.LicenseStatus; +import org.elasticsearch.transport.NoSuchRemoteClusterException; +import org.elasticsearch.transport.RemoteClusterService; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.cluster.metadata.MetaDataCreateIndexService.validateIndexOrAliasName; + +/** + * Validation of source indexes and destination index. + * + * Validations are separated into validators to choose from, e.g. you want to run different types of validations for + * preview/create/start with or without support for remote clusters + */ +public final class SourceDestValidator { + + // messages + public static final String SOURCE_INDEX_MISSING = "Source index [{0}] does not exist"; + public static final String SOURCE_LOWERCASE = "Source index [{0}] must be lowercase"; + public static final String DEST_IN_SOURCE = "Destination index [{0}] is included in source expression [{1}]"; + public static final String DEST_LOWERCASE = "Destination index [{0}] must be lowercase"; + public static final String NEEDS_REMOTE_CLUSTER_SEARCH = "Source index is configured with a remote index pattern(s) [{0}]" + + " but the current node [{1}] is not allowed to connect to remote clusters." + + " Please enable cluster.remote.connect for all data nodes."; + public static final String ERROR_REMOTE_CLUSTER_SEARCH = "Error resolving remote source: {0}"; + public static final String UNKNOWN_REMOTE_CLUSTER_LICENSE = "Error during license check ({0}) for remote cluster " + + "alias(es) {1}, error: {2}"; + public static final String FEATURE_NOT_LICENSED_REMOTE_CLUSTER_LICENSE = "License check failed for remote cluster " + + "alias [{0}], at least a [{1}] license is required, found license [{2}]"; + public static final String REMOTE_CLUSTER_LICENSE_INACTIVE = "License check failed for remote cluster " + + "alias [{0}], license is not active"; + + private final IndexNameExpressionResolver indexNameExpressionResolver; + private final RemoteClusterService remoteClusterService; + private final RemoteClusterLicenseChecker remoteClusterLicenseChecker; + private final String nodeName; + private final String license; + + /* + * Internal shared context between validators. + */ + static class Context { + private final ClusterState state; + private final IndexNameExpressionResolver indexNameExpressionResolver; + private final RemoteClusterService remoteClusterService; + private final RemoteClusterLicenseChecker remoteClusterLicenseChecker; + private final String[] source; + private final String dest; + private final String nodeName; + private final String license; + + private ValidationException validationException = null; + private SortedSet resolvedSource = null; + private SortedSet resolvedRemoteSource = null; + private String resolvedDest = null; + + Context( + final ClusterState state, + final IndexNameExpressionResolver indexNameExpressionResolver, + final RemoteClusterService remoteClusterService, + final RemoteClusterLicenseChecker remoteClusterLicenseChecker, + final String[] source, + final String dest, + final String nodeName, + final String license + ) { + this.state = state; + this.indexNameExpressionResolver = indexNameExpressionResolver; + this.remoteClusterService = remoteClusterService; + this.remoteClusterLicenseChecker = remoteClusterLicenseChecker; + this.source = source; + this.dest = dest; + this.nodeName = nodeName; + this.license = license; + } + + public ClusterState getState() { + return state; + } + + public RemoteClusterService getRemoteClusterService() { + return remoteClusterService; + } + + public RemoteClusterLicenseChecker getRemoteClusterLicenseChecker() { + return remoteClusterLicenseChecker; + } + + public IndexNameExpressionResolver getIndexNameExpressionResolver() { + return indexNameExpressionResolver; + } + + public boolean isRemoteSearchEnabled() { + return remoteClusterLicenseChecker != null; + } + + public String[] getSource() { + return source; + } + + public String getDest() { + return dest; + } + + public String getNodeName() { + return nodeName; + } + + public String getLicense() { + return license; + } + + public SortedSet resolveSource() { + if (resolvedSource == null) { + resolveLocalAndRemoteSource(); + } + + return resolvedSource; + } + + public SortedSet resolveRemoteSource() { + if (resolvedRemoteSource == null) { + resolveLocalAndRemoteSource(); + } + + return resolvedRemoteSource; + } + + public String resolveDest() { + if (resolvedDest == null) { + try { + Index singleWriteIndex = indexNameExpressionResolver.concreteWriteIndex( + state, + IndicesOptions.lenientExpandOpen(), + dest, + true + ); + + resolvedDest = singleWriteIndex != null ? singleWriteIndex.getName() : dest; + } catch (IllegalArgumentException e) { + // stop here as we can not return a single dest index + addValidationError(e.getMessage()); + throw validationException; + } + } + + return resolvedDest; + } + + public ValidationException addValidationError(String error, Object... args) { + if (validationException == null) { + validationException = new ValidationException(); + } + + validationException.addValidationError(getMessage(error, args)); + + return validationException; + } + + public ValidationException getValidationException() { + return validationException; + } + + // convenience method to make testing easier + public Set getRegisteredRemoteClusterNames() { + return remoteClusterService.getRegisteredRemoteClusterNames(); + } + + private void resolveLocalAndRemoteSource() { + resolvedSource = new TreeSet<>(Arrays.asList(source)); + resolvedRemoteSource = new TreeSet<>(RemoteClusterLicenseChecker.remoteIndices(resolvedSource)); + resolvedSource.removeAll(resolvedRemoteSource); + + // special case: if indexNameExpressionResolver gets an empty list it treats it as _all + if (resolvedSource.isEmpty() == false) { + resolvedSource = new TreeSet<>( + Arrays.asList( + indexNameExpressionResolver.concreteIndexNames( + state, + DEFAULT_INDICES_OPTIONS_FOR_VALIDATION, + resolvedSource.toArray(new String[0]) + ) + ) + ); + } + } + } + + interface SourceDestValidation { + void validate(Context context, ActionListener listener); + } + + // note: this is equivalent to the default for search requests + private static final IndicesOptions DEFAULT_INDICES_OPTIONS_FOR_VALIDATION = IndicesOptions + .strictExpandOpenAndForbidClosedIgnoreThrottled(); + + public static final SourceDestValidation SOURCE_MISSING_VALIDATION = new SourceMissingValidation(); + public static final SourceDestValidation REMOTE_SOURCE_VALIDATION = new RemoteSourceEnabledAndRemoteLicenseValidation(); + public static final SourceDestValidation DESTINATION_IN_SOURCE_VALIDATION = new DestinationInSourceValidation(); + public static final SourceDestValidation DESTINATION_SINGLE_INDEX_VALIDATION = new DestinationSingleIndexValidation(); + + // set of default validation collections, if you want to automatically benefit from new validators, use those + public static final List PREVIEW_VALIDATIONS = Arrays.asList(SOURCE_MISSING_VALIDATION, REMOTE_SOURCE_VALIDATION); + + public static final List ALL_VALIDATIONS = Arrays.asList( + SOURCE_MISSING_VALIDATION, + REMOTE_SOURCE_VALIDATION, + DESTINATION_IN_SOURCE_VALIDATION, + DESTINATION_SINGLE_INDEX_VALIDATION + ); + + public static final List NON_DEFERABLE_VALIDATIONS = Arrays.asList(DESTINATION_SINGLE_INDEX_VALIDATION); + + /** + * Create a new Source Dest Validator + * + * @param indexNameExpressionResolver A valid IndexNameExpressionResolver object + * @param remoteClusterService A valid RemoteClusterService object + * @param remoteClusterLicenseChecker A RemoteClusterLicenseChecker or null if CCS is disabled + * @param nodeName the name of this node + * @param license the license of the feature validated for + */ + public SourceDestValidator( + IndexNameExpressionResolver indexNameExpressionResolver, + RemoteClusterService remoteClusterService, + RemoteClusterLicenseChecker remoteClusterLicenseChecker, + String nodeName, + String license + ) { + this.indexNameExpressionResolver = indexNameExpressionResolver; + this.remoteClusterService = remoteClusterService; + this.remoteClusterLicenseChecker = remoteClusterLicenseChecker; + this.nodeName = nodeName; + this.license = license; + } + + /** + * Run validation against source and dest. + * + * @param clusterState The current ClusterState + * @param source an array of source indexes + * @param dest destination index + * @param validations list of of validations to run + * @param listener result listener + */ + public void validate( + final ClusterState clusterState, + final String[] source, + final String dest, + final List validations, + final ActionListener listener + ) { + Context context = new Context( + clusterState, + indexNameExpressionResolver, + remoteClusterService, + remoteClusterLicenseChecker, + source, + dest, + nodeName, + license + ); + + ActionListener validationListener = ActionListener.wrap(c -> { + if (c.getValidationException() != null) { + listener.onFailure(c.getValidationException()); + } else { + listener.onResponse(true); + } + }, listener::onFailure); + + for (int i = validations.size() - 1; i >= 0; i--) { + final SourceDestValidation validation = validations.get(i); + final ActionListener previousValidationListener = validationListener; + validationListener = ActionListener.wrap(c -> { validation.validate(c, previousValidationListener); }, listener::onFailure); + } + + validationListener.onResponse(context); + } + + /** + * Validate dest request. + * + * This runs a couple of simple validations at request time, to be executed from a {@link ActionRequest}} + * implementation. + * + * Note: Source can not be validated at request time as it might contain expressions. + * + * @param validationException an ActionRequestValidationException for collection validation problem, can be null + * @param dest destination index, null if validation shall be skipped + */ + public static ActionRequestValidationException validateRequest( + @Nullable ActionRequestValidationException validationException, + @Nullable String dest + ) { + try { + if (dest != null) { + validateIndexOrAliasName(dest, InvalidIndexNameException::new); + if (dest.toLowerCase(Locale.ROOT).equals(dest) == false) { + validationException = addValidationError(getMessage(DEST_LOWERCASE, dest), validationException); + } + } + } catch (InvalidIndexNameException ex) { + validationException = addValidationError(ex.getMessage(), validationException); + } + + return validationException; + } + + static class SourceMissingValidation implements SourceDestValidation { + + @Override + public void validate(Context context, ActionListener listener) { + try { + // non-trivia: if source contains a wildcard index, which does not resolve to a concrete index + // the resolved indices might be empty, but we can check if source contained something, this works because + // of no wildcard index is involved the resolve would have thrown an exception + if (context.resolveSource().isEmpty() && context.resolveRemoteSource().isEmpty() && context.getSource().length == 0) { + context.addValidationError(SOURCE_INDEX_MISSING, Strings.arrayToCommaDelimitedString(context.getSource())); + } + } catch (IndexNotFoundException e) { + context.addValidationError(e.getMessage()); + } + listener.onResponse(context); + } + } + + static class RemoteSourceEnabledAndRemoteLicenseValidation implements SourceDestValidation { + @Override + public void validate(Context context, ActionListener listener) { + if (context.resolveRemoteSource().isEmpty()) { + listener.onResponse(context); + return; + } + + List remoteIndices = new ArrayList<>(context.resolveRemoteSource()); + // we can only check this node at the moment, clusters with mixed CCS enabled/disabled nodes are not supported, + // see gh#50033 + if (context.isRemoteSearchEnabled() == false) { + context.addValidationError(NEEDS_REMOTE_CLUSTER_SEARCH, context.resolveRemoteSource(), context.getNodeName()); + listener.onResponse(context); + return; + } + + // this can throw + List remoteAliases; + try { + remoteAliases = RemoteClusterLicenseChecker.remoteClusterAliases(context.getRegisteredRemoteClusterNames(), remoteIndices); + } catch (NoSuchRemoteClusterException e) { + context.addValidationError(e.getMessage()); + listener.onResponse(context); + return; + } catch (Exception e) { + context.addValidationError(ERROR_REMOTE_CLUSTER_SEARCH, e.getMessage()); + listener.onResponse(context); + return; + } + + context.getRemoteClusterLicenseChecker().checkRemoteClusterLicenses(remoteAliases, ActionListener.wrap(response -> { + if (response.isSuccess() == false) { + if (response.remoteClusterLicenseInfo().licenseInfo().getStatus() != LicenseStatus.ACTIVE) { + context.addValidationError(REMOTE_CLUSTER_LICENSE_INACTIVE, response.remoteClusterLicenseInfo().clusterAlias()); + } else { + context.addValidationError( + FEATURE_NOT_LICENSED_REMOTE_CLUSTER_LICENSE, + response.remoteClusterLicenseInfo().clusterAlias(), + context.getLicense(), + response.remoteClusterLicenseInfo().licenseInfo().getType() + ); + } + } + listener.onResponse(context); + }, e -> { + context.addValidationError(UNKNOWN_REMOTE_CLUSTER_LICENSE, context.getLicense(), remoteAliases, e.getMessage()); + listener.onResponse(context); + })); + } + } + + static class DestinationInSourceValidation implements SourceDestValidation { + + @Override + public void validate(Context context, ActionListener listener) { + final String destIndex = context.getDest(); + boolean foundSourceInDest = false; + + for (String src : context.getSource()) { + if (Regex.simpleMatch(src, destIndex)) { + context.addValidationError(DEST_IN_SOURCE, destIndex, src); + // do not return immediately but collect all errors and than return + foundSourceInDest = true; + } + } + + if (foundSourceInDest) { + listener.onResponse(context); + return; + } + + if (context.resolvedSource.contains(destIndex)) { + context.addValidationError(DEST_IN_SOURCE, destIndex, Strings.arrayToCommaDelimitedString(context.getSource())); + listener.onResponse(context); + return; + } + + if (context.resolvedSource.contains(context.resolveDest())) { + context.addValidationError( + DEST_IN_SOURCE, + context.resolveDest(), + Strings.collectionToCommaDelimitedString(context.resolveSource()) + ); + } + + listener.onResponse(context); + } + } + + static class DestinationSingleIndexValidation implements SourceDestValidation { + + @Override + public void validate(Context context, ActionListener listener) { + context.resolveDest(); + listener.onResponse(context); + } + } + + private static String getMessage(String message, Object... args) { + return new MessageFormat(message, Locale.ROOT).format(args); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformMessages.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformMessages.java index 9708cd301e437..b1e9dffea5027 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformMessages.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformMessages.java @@ -21,9 +21,6 @@ public class TransformMessages { "Failed to validate configuration"; public static final String REST_PUT_FAILED_PERSIST_TRANSFORM_CONFIGURATION = "Failed to persist transform configuration"; public static final String REST_PUT_TRANSFORM_FAILED_TO_DEDUCE_DEST_MAPPINGS = "Failed to deduce dest mappings"; - public static final String REST_PUT_TRANSFORM_SOURCE_INDEX_MISSING = "Source index [{0}] does not exist"; - public static final String REST_PUT_TRANSFORM_DEST_IN_SOURCE = "Destination index [{0}] is included in source expression [{1}]"; - public static final String REST_PUT_TRANSFORM_DEST_SINGLE_INDEX = "Destination index [{0}] should refer to a single index"; public static final String REST_PUT_TRANSFORM_INCONSISTENT_ID = "Inconsistent id; ''{0}'' specified in the body differs from ''{1}'' specified as a URL argument"; public static final String TRANSFORM_CONFIG_INVALID = "Transform configuration is invalid [{0}]"; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java index cb095be157193..9078d5952e2af 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java @@ -22,6 +22,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.xpack.core.common.validation.SourceDestValidator; import org.elasticsearch.xpack.core.transform.TransformField; import org.elasticsearch.xpack.core.transform.transforms.DestConfig; import org.elasticsearch.xpack.core.transform.transforms.TransformConfig; @@ -66,7 +67,7 @@ public static Request fromXContent(final XContentParser parser) throws IOExcepti Object providedDestination = content.get(TransformField.DESTINATION.getPreferredName()); if (providedDestination instanceof Map) { @SuppressWarnings("unchecked") - Map destMap = (Map)providedDestination; + Map destMap = (Map) providedDestination; String pipeline = destMap.get(DestConfig.PIPELINE.getPreferredName()); if (pipeline != null) { tempDestination.put(DestConfig.PIPELINE.getPreferredName(), pipeline); @@ -74,12 +75,15 @@ public static Request fromXContent(final XContentParser parser) throws IOExcepti } content.put(TransformField.DESTINATION.getPreferredName(), tempDestination); content.put(TransformField.ID.getPreferredName(), "transform-preview"); - try(XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().map(content); - XContentParser newParser = XContentType.JSON - .xContent() - .createParser(parser.getXContentRegistry(), + try ( + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().map(content); + XContentParser newParser = XContentType.JSON.xContent() + .createParser( + parser.getXContentRegistry(), LoggingDeprecationHandler.INSTANCE, - BytesReference.bytes(xContentBuilder).streamInput())) { + BytesReference.bytes(xContentBuilder).streamInput() + ) + ) { return new Request(TransformConfig.fromXContent(newParser, "transform-preview", false)); } } @@ -87,15 +91,20 @@ public static Request fromXContent(final XContentParser parser) throws IOExcepti @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; - if(config.getPivotConfig() != null) { - for(String failure : config.getPivotConfig().aggFieldValidation()) { + if (config.getPivotConfig() != null) { + for (String failure : config.getPivotConfig().aggFieldValidation()) { validationException = addValidationError(failure, validationException); } } + + validationException = SourceDestValidator.validateRequest( + validationException, + config.getDestination() != null ? config.getDestination().getIndex() : null + ); + return validationException; } - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { return this.config.toXContent(builder, params); @@ -141,6 +150,7 @@ public static class Response extends ActionResponse implements ToXContentObject PARSER.declareObjectArray(Response::setDocs, (p, c) -> p.mapOrdered(), PREVIEW); PARSER.declareObject(Response::setMappings, (p, c) -> p.mapOrdered(), MAPPINGS); } + public Response() {} public Response(StreamInput in) throws IOException { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PutTransformAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PutTransformAction.java index 734d1c9b0b8d8..5199ca0601e6a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PutTransformAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PutTransformAction.java @@ -15,18 +15,16 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.indices.InvalidIndexNameException; +import org.elasticsearch.xpack.core.common.validation.SourceDestValidator; import org.elasticsearch.xpack.core.transform.TransformField; import org.elasticsearch.xpack.core.transform.TransformMessages; import org.elasticsearch.xpack.core.transform.transforms.TransformConfig; import org.elasticsearch.xpack.core.transform.utils.TransformStrings; import java.io.IOException; -import java.util.Locale; import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; -import static org.elasticsearch.cluster.metadata.MetaDataCreateIndexService.validateIndexOrAliasName; public class PutTransformAction extends ActionType { @@ -71,46 +69,46 @@ public static Request fromXContent(final XContentParser parser, final String id, @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; - if(config.getPivotConfig() != null + if (config.getPivotConfig() != null && config.getPivotConfig().getMaxPageSearchSize() != null && (config.getPivotConfig().getMaxPageSearchSize() < 10 || config.getPivotConfig().getMaxPageSearchSize() > 10_000)) { validationException = addValidationError( - "pivot.max_page_search_size [" + - config.getPivotConfig().getMaxPageSearchSize() + "] must be greater than 10 and less than 10,000", - validationException); + "pivot.max_page_search_size [" + + config.getPivotConfig().getMaxPageSearchSize() + + "] must be greater than 10 and less than 10,000", + validationException + ); } - for(String failure : config.getPivotConfig().aggFieldValidation()) { + for (String failure : config.getPivotConfig().aggFieldValidation()) { validationException = addValidationError(failure, validationException); } - String destIndex = config.getDestination().getIndex(); - try { - validateIndexOrAliasName(destIndex, InvalidIndexNameException::new); - if (!destIndex.toLowerCase(Locale.ROOT).equals(destIndex)) { - validationException = addValidationError("dest.index [" + destIndex +"] must be lowercase", validationException); - } - } catch (InvalidIndexNameException ex) { - validationException = addValidationError(ex.getMessage(), validationException); - } + + validationException = SourceDestValidator.validateRequest(validationException, config.getDestination().getIndex()); + if (TransformStrings.isValidId(config.getId()) == false) { validationException = addValidationError( TransformMessages.getMessage(TransformMessages.INVALID_ID, TransformField.ID.getPreferredName(), config.getId()), - validationException); + validationException + ); } if (TransformStrings.hasValidLengthForId(config.getId()) == false) { validationException = addValidationError( TransformMessages.getMessage(TransformMessages.ID_TOO_LONG, TransformStrings.ID_LENGTH_LIMIT), - validationException); + validationException + ); } TimeValue frequency = config.getFrequency(); if (frequency != null) { if (frequency.compareTo(MIN_FREQUENCY) < 0) { validationException = addValidationError( "minimum permitted [" + TransformField.FREQUENCY + "] is [" + MIN_FREQUENCY.getStringRep() + "]", - validationException); + validationException + ); } else if (frequency.compareTo(MAX_FREQUENCY) > 0) { validationException = addValidationError( "highest permitted [" + TransformField.FREQUENCY + "] is [" + MAX_FREQUENCY.getStringRep() + "]", - validationException); + validationException + ); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/UpdateTransformAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/UpdateTransformAction.java index b8cc02949834d..81fdad0df2b63 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/UpdateTransformAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/UpdateTransformAction.java @@ -16,17 +16,15 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.indices.InvalidIndexNameException; +import org.elasticsearch.xpack.core.common.validation.SourceDestValidator; import org.elasticsearch.xpack.core.transform.TransformField; import org.elasticsearch.xpack.core.transform.transforms.TransformConfig; import org.elasticsearch.xpack.core.transform.transforms.TransformConfigUpdate; import java.io.IOException; -import java.util.Locale; import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; -import static org.elasticsearch.cluster.metadata.MetaDataCreateIndexService.validateIndexOrAliasName; public class UpdateTransformAction extends ActionType { @@ -46,7 +44,7 @@ public static class Request extends AcknowledgedRequest { private final String id; private final boolean deferValidation; - public Request(TransformConfigUpdate update, String id, boolean deferValidation) { + public Request(TransformConfigUpdate update, String id, boolean deferValidation) { this.update = update; this.id = id; this.deferValidation = deferValidation; @@ -70,27 +68,24 @@ public static Request fromXContent(final XContentParser parser, final String id, @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; + if (update.getDestination() != null && update.getDestination().getIndex() != null) { - String destIndex = update.getDestination().getIndex(); - try { - validateIndexOrAliasName(destIndex, InvalidIndexNameException::new); - if (!destIndex.toLowerCase(Locale.ROOT).equals(destIndex)) { - validationException = addValidationError("dest.index [" + destIndex + "] must be lowercase", validationException); - } - } catch (InvalidIndexNameException ex) { - validationException = addValidationError(ex.getMessage(), validationException); - } + + validationException = SourceDestValidator.validateRequest(validationException, update.getDestination().getIndex()); } + TimeValue frequency = update.getFrequency(); if (frequency != null) { if (frequency.compareTo(MIN_FREQUENCY) < 0) { validationException = addValidationError( "minimum permitted [" + TransformField.FREQUENCY + "] is [" + MIN_FREQUENCY.getStringRep() + "]", - validationException); + validationException + ); } else if (frequency.compareTo(MAX_FREQUENCY) > 0) { validationException = addValidationError( "highest permitted [" + TransformField.FREQUENCY + "] is [" + MAX_FREQUENCY.getStringRep() + "]", - validationException); + validationException + ); } } @@ -131,9 +126,7 @@ public boolean equals(Object obj) { return false; } Request other = (Request) obj; - return Objects.equals(update, other.update) && - this.deferValidation == other.deferValidation && - this.id.equals(other.id); + return Objects.equals(update, other.update) && this.deferValidation == other.deferValidation && this.id.equals(other.id); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/common/validation/SourceDestValidatorTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/common/validation/SourceDestValidatorTests.java new file mode 100644 index 0000000000000..566925258c879 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/common/validation/SourceDestValidatorTests.java @@ -0,0 +1,822 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.common.validation; + +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.LatchedActionListener; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.AliasMetaData; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.CheckedConsumer; +import org.elasticsearch.common.ValidationException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.License; +import org.elasticsearch.license.RemoteClusterLicenseChecker; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.protocol.xpack.XPackInfoRequest; +import org.elasticsearch.protocol.xpack.XPackInfoResponse; +import org.elasticsearch.protocol.xpack.XPackInfoResponse.LicenseInfo; +import org.elasticsearch.protocol.xpack.license.LicenseStatus; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.client.NoOpClient; +import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteClusterService; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.common.validation.SourceDestValidator.Context; +import org.elasticsearch.xpack.core.common.validation.SourceDestValidator.RemoteSourceEnabledAndRemoteLicenseValidation; +import org.junit.After; +import org.junit.Before; + +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_CREATION_DATE; +import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS; +import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS; +import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_VERSION_CREATED; +import static org.elasticsearch.mock.orig.Mockito.when; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.spy; + +public class SourceDestValidatorTests extends ESTestCase { + + private static final String SOURCE_1 = "source-1"; + private static final String SOURCE_1_ALIAS = "source-1-alias"; + private static final String SOURCE_2 = "source-2"; + private static final String DEST_ALIAS = "dest-alias"; + private static final String ALIASED_DEST = "aliased-dest"; + private static final String ALIAS_READ_WRITE_DEST = "alias-read-write-dest"; + private static final String REMOTE_BASIC = "remote-basic"; + private static final String REMOTE_PLATINUM = "remote-platinum"; + + private static final ClusterState CLUSTER_STATE; + private Client clientWithBasicLicense; + private Client clientWithExpiredBasicLicense; + private Client clientWithPlatinumLicense; + private Client clientWithTrialLicense; + private RemoteClusterLicenseChecker remoteClusterLicenseCheckerBasic; + + private final ThreadPool threadPool = new TestThreadPool(getClass().getName()); + private final TransportService transportService = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool); + private final RemoteClusterService remoteClusterService = transportService.getRemoteClusterService(); + private final SourceDestValidator simpleNonRemoteValidator = new SourceDestValidator( + new IndexNameExpressionResolver(), + remoteClusterService, + null, + "node_id", + "license" + ); + + static { + IndexMetaData source1 = IndexMetaData.builder(SOURCE_1) + .settings( + Settings.builder() + .put(SETTING_VERSION_CREATED, Version.CURRENT) + .put(SETTING_NUMBER_OF_SHARDS, 1) + .put(SETTING_NUMBER_OF_REPLICAS, 0) + .put(SETTING_CREATION_DATE, System.currentTimeMillis()) + ) + .putAlias(AliasMetaData.builder(SOURCE_1_ALIAS).build()) + .putAlias(AliasMetaData.builder(ALIAS_READ_WRITE_DEST).writeIndex(false).build()) + .build(); + IndexMetaData source2 = IndexMetaData.builder(SOURCE_2) + .settings( + Settings.builder() + .put(SETTING_VERSION_CREATED, Version.CURRENT) + .put(SETTING_NUMBER_OF_SHARDS, 1) + .put(SETTING_NUMBER_OF_REPLICAS, 0) + .put(SETTING_CREATION_DATE, System.currentTimeMillis()) + ) + .putAlias(AliasMetaData.builder(DEST_ALIAS).build()) + .putAlias(AliasMetaData.builder(ALIAS_READ_WRITE_DEST).writeIndex(false).build()) + .build(); + IndexMetaData aliasedDest = IndexMetaData.builder(ALIASED_DEST) + .settings( + Settings.builder() + .put(SETTING_VERSION_CREATED, Version.CURRENT) + .put(SETTING_NUMBER_OF_SHARDS, 1) + .put(SETTING_NUMBER_OF_REPLICAS, 0) + .put(SETTING_CREATION_DATE, System.currentTimeMillis()) + ) + .putAlias(AliasMetaData.builder(DEST_ALIAS).build()) + .putAlias(AliasMetaData.builder(ALIAS_READ_WRITE_DEST).build()) + .build(); + ClusterState.Builder state = ClusterState.builder(new ClusterName("test")); + state.metaData( + MetaData.builder() + .put(IndexMetaData.builder(source1).build(), false) + .put(IndexMetaData.builder(source2).build(), false) + .put(IndexMetaData.builder(aliasedDest).build(), false) + ); + CLUSTER_STATE = state.build(); + } + + private class MockClientLicenseCheck extends NoOpClient { + private final String license; + private final LicenseStatus licenseStatus; + + MockClientLicenseCheck(String testName, String license, LicenseStatus licenseStatus) { + super(testName); + this.license = license; + this.licenseStatus = licenseStatus; + } + + @Override + public Client getRemoteClusterClient(String clusterAlias) { + return this; + } + + @SuppressWarnings("unchecked") + @Override + protected void doExecute( + ActionType action, + Request request, + ActionListener listener + ) { + if (request instanceof XPackInfoRequest) { + XPackInfoResponse response = new XPackInfoResponse( + null, + new LicenseInfo("uid", license, license, licenseStatus, randomNonNegativeLong()), + null + ); + listener.onResponse((Response) response); + return; + } + super.doExecute(action, request, listener); + } + } + + @Before + public void setupComponents() { + clientWithBasicLicense = new MockClientLicenseCheck(getTestName(), "basic", LicenseStatus.ACTIVE); + clientWithExpiredBasicLicense = new MockClientLicenseCheck(getTestName(), "basic", LicenseStatus.EXPIRED); + remoteClusterLicenseCheckerBasic = new RemoteClusterLicenseChecker( + clientWithBasicLicense, + (operationMode -> operationMode != License.OperationMode.MISSING) + ); + clientWithPlatinumLicense = new MockClientLicenseCheck(getTestName(), "platinum", LicenseStatus.ACTIVE); + clientWithTrialLicense = new MockClientLicenseCheck(getTestName(), "trial", LicenseStatus.ACTIVE); + } + + @After + public void closeComponents() throws Exception { + clientWithBasicLicense.close(); + clientWithExpiredBasicLicense.close(); + clientWithPlatinumLicense.close(); + clientWithTrialLicense.close(); + ThreadPool.terminate(threadPool, 10, TimeUnit.SECONDS); + } + + public void testCheck_GivenSimpleSourceIndexAndValidDestIndex() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1 }, + "dest", + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + true, + null + ); + } + + public void testCheck_GivenNoSourceIndexAndValidDestIndex() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] {}, + "dest", + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat(e.validationErrors().get(0), equalTo("Source index [] does not exist")); + } + ); + } + + public void testCheck_GivenMissingConcreteSourceIndex() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { "missing" }, + "dest", + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat(e.validationErrors().get(0), equalTo("no such index [missing]")); + } + ); + + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { "missing" }, + "dest", + SourceDestValidator.NON_DEFERABLE_VALIDATIONS, + listener + ), + true, + null + ); + } + + public void testCheck_GivenMixedMissingAndExistingConcreteSourceIndex() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1, "missing" }, + "dest", + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat(e.validationErrors().get(0), equalTo("no such index [missing]")); + } + ); + + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1, "missing" }, + "dest", + SourceDestValidator.NON_DEFERABLE_VALIDATIONS, + listener + ), + true, + null + ); + } + + public void testCheck_GivenMixedMissingWildcardExistingConcreteSourceIndex() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1, "wildcard*", "missing" }, + "dest", + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat(e.validationErrors().get(0), equalTo("no such index [missing]")); + } + ); + + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1, "wildcard*", "missing" }, + "dest", + SourceDestValidator.NON_DEFERABLE_VALIDATIONS, + listener + ), + true, + null + ); + } + + public void testCheck_GivenWildcardSourceIndex() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { "wildcard*" }, + "dest", + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + true, + null + ); + } + + public void testCheck_GivenDestIndexSameAsSourceIndex() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1 }, + SOURCE_1, + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat( + e.validationErrors().get(0), + equalTo("Destination index [" + SOURCE_1 + "] is included in source expression [" + SOURCE_1 + "]") + ); + } + ); + + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1 }, + SOURCE_1, + SourceDestValidator.NON_DEFERABLE_VALIDATIONS, + listener + ), + true, + null + ); + } + + public void testCheck_GivenDestIndexMatchesSourceIndex() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { "source-*" }, + SOURCE_2, + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat( + e.validationErrors().get(0), + equalTo("Destination index [" + SOURCE_2 + "] is included in source expression [source-*]") + ); + } + ); + + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { "source-*" }, + SOURCE_2, + SourceDestValidator.NON_DEFERABLE_VALIDATIONS, + listener + ), + true, + null + ); + } + + public void testCheck_GivenDestIndexMatchesOneOfSourceIndices() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { "source-1", "source-*" }, + SOURCE_2, + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat( + e.validationErrors().get(0), + equalTo("Destination index [" + SOURCE_2 + "] is included in source expression [source-*]") + ); + } + ); + + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { "source-1", "source-*" }, + SOURCE_2, + SourceDestValidator.NON_DEFERABLE_VALIDATIONS, + listener + ), + true, + null + ); + } + + public void testCheck_GivenDestIndexMatchesMultipleSourceIndices() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { "source-1", "source-*", "sou*" }, + SOURCE_2, + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(2, e.validationErrors().size()); + assertThat( + e.validationErrors().get(0), + equalTo("Destination index [" + SOURCE_2 + "] is included in source expression [source-*]") + ); + assertThat( + e.validationErrors().get(1), + equalTo("Destination index [" + SOURCE_2 + "] is included in source expression [sou*]") + ); + } + ); + } + + public void testCheck_GivenDestIndexIsAliasThatMatchesMultipleIndices() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1 }, + DEST_ALIAS, + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat( + e.validationErrors().get(0), + equalTo( + "no write index is defined for alias [dest-alias]. " + + "The write index may be explicitly disabled using is_write_index=false or the alias points " + + "to multiple indices without one being designated as a write index" + ) + ); + } + ); + + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1 }, + DEST_ALIAS, + SourceDestValidator.NON_DEFERABLE_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat( + e.validationErrors().get(0), + equalTo( + "no write index is defined for alias [dest-alias]. " + + "The write index may be explicitly disabled using is_write_index=false or the alias points " + + "to multiple indices without one being designated as a write index" + ) + ); + } + ); + } + + public void testCheck_GivenDestIndexIsAliasThatMatchesMultipleIndicesButHasSingleWriteAlias() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1 }, + ALIAS_READ_WRITE_DEST, + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat( + e.validationErrors().get(0), + equalTo( + "no write index is defined for alias [" + + ALIAS_READ_WRITE_DEST + + "]. " + + "The write index may be explicitly disabled using is_write_index=false or the alias points " + + "to multiple indices without one being designated as a write index" + ) + ); + } + ); + } + + public void testCheck_GivenDestIndexIsAliasThatIsIncludedInSource() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1 }, + SOURCE_1_ALIAS, + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(1, e.validationErrors().size()); + assertThat( + e.validationErrors().get(0), + equalTo("Destination index [" + SOURCE_1 + "] is included in source expression [" + SOURCE_1 + "]") + ); + } + ); + + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1 }, + SOURCE_1_ALIAS, + SourceDestValidator.NON_DEFERABLE_VALIDATIONS, + listener + ), + true, + null + ); + } + + public void testCheck_MultipleValidationErrors() throws InterruptedException { + assertValidation( + listener -> simpleNonRemoteValidator.validate( + CLUSTER_STATE, + new String[] { SOURCE_1, "missing" }, + SOURCE_1_ALIAS, + SourceDestValidator.ALL_VALIDATIONS, + listener + ), + (Boolean) null, + e -> { + assertEquals(2, e.validationErrors().size()); + assertThat(e.validationErrors().get(0), equalTo("no such index [missing]")); + assertThat( + e.validationErrors().get(1), + equalTo("Destination index [" + SOURCE_1 + "] is included in source expression [missing,source-1]") + ); + } + ); + } + + // CCS tests: at time of writing it wasn't possible to mock RemoteClusterService, therefore it's not possible + // to test the whole validation but test RemoteSourceEnabledAndRemoteLicenseValidation + public void testRemoteSourceBasic() throws InterruptedException { + Context context = spy( + new SourceDestValidator.Context( + CLUSTER_STATE, + new IndexNameExpressionResolver(), + remoteClusterService, + remoteClusterLicenseCheckerBasic, + new String[] { REMOTE_BASIC + ":" + "SOURCE_1" }, + "dest", + "node_id", + "license" + ) + ); + + when(context.getRegisteredRemoteClusterNames()).thenReturn(Collections.singleton(REMOTE_BASIC)); + RemoteSourceEnabledAndRemoteLicenseValidation validator = new RemoteSourceEnabledAndRemoteLicenseValidation(); + + assertValidationWithContext( + listener -> validator.validate(context, listener), + c -> { assertNull(c.getValidationException()); }, + null + ); + } + + public void testRemoteSourcePlatinum() throws InterruptedException { + final Context context = spy( + new SourceDestValidator.Context( + CLUSTER_STATE, + new IndexNameExpressionResolver(), + remoteClusterService, + new RemoteClusterLicenseChecker(clientWithBasicLicense, XPackLicenseState::isPlatinumOrTrialOperationMode), + new String[] { REMOTE_BASIC + ":" + "SOURCE_1" }, + "dest", + "node_id", + "platinum" + ) + ); + + when(context.getRegisteredRemoteClusterNames()).thenReturn(Collections.singleton(REMOTE_BASIC)); + final RemoteSourceEnabledAndRemoteLicenseValidation validator = new RemoteSourceEnabledAndRemoteLicenseValidation(); + + assertValidationWithContext(listener -> validator.validate(context, listener), c -> { + assertNotNull(c.getValidationException()); + assertEquals(1, c.getValidationException().validationErrors().size()); + assertThat( + c.getValidationException().validationErrors().get(0), + equalTo( + "License check failed for remote cluster alias [" + + REMOTE_BASIC + + "], at least a [platinum] license is required, found license [basic]" + ) + ); + }, null); + + final Context context2 = spy( + new SourceDestValidator.Context( + CLUSTER_STATE, + new IndexNameExpressionResolver(), + remoteClusterService, + new RemoteClusterLicenseChecker(clientWithPlatinumLicense, XPackLicenseState::isPlatinumOrTrialOperationMode), + new String[] { REMOTE_PLATINUM + ":" + "SOURCE_1" }, + "dest", + "node_id", + "license" + ) + ); + when(context2.getRegisteredRemoteClusterNames()).thenReturn(Collections.singleton(REMOTE_PLATINUM)); + + assertValidationWithContext( + listener -> validator.validate(context2, listener), + c -> { assertNull(c.getValidationException()); }, + null + ); + + final Context context3 = spy( + new SourceDestValidator.Context( + CLUSTER_STATE, + new IndexNameExpressionResolver(), + remoteClusterService, + new RemoteClusterLicenseChecker(clientWithPlatinumLicense, XPackLicenseState::isPlatinumOrTrialOperationMode), + new String[] { REMOTE_PLATINUM + ":" + "SOURCE_1" }, + "dest", + "node_id", + "platinum" + ) + ); + when(context3.getRegisteredRemoteClusterNames()).thenReturn(Collections.singleton(REMOTE_PLATINUM)); + + final RemoteSourceEnabledAndRemoteLicenseValidation validator3 = new RemoteSourceEnabledAndRemoteLicenseValidation(); + assertValidationWithContext( + listener -> validator3.validate(context3, listener), + c -> { assertNull(c.getValidationException()); }, + null + ); + + final Context context4 = spy( + new SourceDestValidator.Context( + CLUSTER_STATE, + new IndexNameExpressionResolver(), + remoteClusterService, + new RemoteClusterLicenseChecker(clientWithTrialLicense, XPackLicenseState::isPlatinumOrTrialOperationMode), + new String[] { REMOTE_PLATINUM + ":" + "SOURCE_1" }, + "dest", + "node_id", + "trial" + ) + ); + when(context4.getRegisteredRemoteClusterNames()).thenReturn(Collections.singleton(REMOTE_PLATINUM)); + + final RemoteSourceEnabledAndRemoteLicenseValidation validator4 = new RemoteSourceEnabledAndRemoteLicenseValidation(); + assertValidationWithContext( + listener -> validator4.validate(context4, listener), + c -> { assertNull(c.getValidationException()); }, + null + ); + } + + public void testRemoteSourceLicenseInActive() throws InterruptedException { + final Context context = spy( + new SourceDestValidator.Context( + CLUSTER_STATE, + new IndexNameExpressionResolver(), + remoteClusterService, + new RemoteClusterLicenseChecker(clientWithExpiredBasicLicense, XPackLicenseState::isPlatinumOrTrialOperationMode), + new String[] { REMOTE_BASIC + ":" + "SOURCE_1" }, + "dest", + "node_id", + "license" + ) + ); + + when(context.getRegisteredRemoteClusterNames()).thenReturn(Collections.singleton(REMOTE_BASIC)); + final RemoteSourceEnabledAndRemoteLicenseValidation validator = new RemoteSourceEnabledAndRemoteLicenseValidation(); + assertValidationWithContext(listener -> validator.validate(context, listener), c -> { + assertNotNull(c.getValidationException()); + assertEquals(1, c.getValidationException().validationErrors().size()); + assertThat( + c.getValidationException().validationErrors().get(0), + equalTo("License check failed for remote cluster alias [" + REMOTE_BASIC + "], license is not active") + ); + }, null); + } + + public void testRemoteSourceDoesNotExist() throws InterruptedException { + Context context = spy( + new SourceDestValidator.Context( + CLUSTER_STATE, + new IndexNameExpressionResolver(), + remoteClusterService, + new RemoteClusterLicenseChecker(clientWithExpiredBasicLicense, XPackLicenseState::isPlatinumOrTrialOperationMode), + new String[] { "non_existing_remote:" + "SOURCE_1" }, + "dest", + "node_id", + "license" + ) + ); + + when(context.getRegisteredRemoteClusterNames()).thenReturn(Collections.singleton(REMOTE_BASIC)); + RemoteSourceEnabledAndRemoteLicenseValidation validator = new RemoteSourceEnabledAndRemoteLicenseValidation(); + + assertValidationWithContext(listener -> validator.validate(context, listener), c -> { + assertNotNull(c.getValidationException()); + assertEquals(1, c.getValidationException().validationErrors().size()); + assertThat(c.getValidationException().validationErrors().get(0), equalTo("no such remote cluster: [non_existing_remote]")); + }, null); + } + + public void testRequestValidation() { + ActionRequestValidationException validationException = SourceDestValidator.validateRequest(null, "UPPERCASE"); + assertNotNull(validationException); + assertEquals(1, validationException.validationErrors().size()); + assertThat(validationException.validationErrors().get(0), equalTo("Destination index [UPPERCASE] must be lowercase")); + + validationException = SourceDestValidator.validateRequest(null, "remote:dest"); + assertNotNull(validationException); + assertEquals(1, validationException.validationErrors().size()); + assertThat(validationException.validationErrors().get(0), equalTo("Invalid index name [remote:dest], must not contain ':'")); + + validationException = SourceDestValidator.validateRequest(null, "dest"); + assertNull(validationException); + + validationException = new ActionRequestValidationException(); + validationException.addValidationError("error1"); + validationException.addValidationError("error2"); + validationException = SourceDestValidator.validateRequest(validationException, "dest"); + assertNotNull(validationException); + assertEquals(2, validationException.validationErrors().size()); + assertEquals(validationException.validationErrors().get(0), "error1"); + assertEquals(validationException.validationErrors().get(1), "error2"); + + validationException = SourceDestValidator.validateRequest(validationException, "UPPERCASE"); + assertNotNull(validationException); + assertEquals(3, validationException.validationErrors().size()); + assertThat(validationException.validationErrors().get(2), equalTo("Destination index [UPPERCASE] must be lowercase")); + } + + private void assertValidation(Consumer> function, T expected, Consumer onException) + throws InterruptedException { + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean listenerCalled = new AtomicBoolean(false); + + LatchedActionListener listener = new LatchedActionListener<>(ActionListener.wrap(r -> { + assertTrue("listener called more than once", listenerCalled.compareAndSet(false, true)); + if (expected == null) { + fail("expected an exception but got a response"); + } else { + assertThat(r, equalTo(expected)); + } + }, e -> { + assertTrue("listener called more than once", listenerCalled.compareAndSet(false, true)); + if (onException == null) { + logger.error("got unexpected exception", e); + fail("got unexpected exception: " + e.getMessage()); + } else if (e instanceof ValidationException) { + onException.accept((ValidationException) e); + } else { + fail("got unexpected exception type: " + e); + } + }), latch); + + function.accept(listener); + assertTrue("timed out after 20s", latch.await(20, TimeUnit.SECONDS)); + } + + private void assertValidationWithContext( + Consumer> function, + CheckedConsumer onAnswer, + Consumer onException + ) throws InterruptedException { + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean listenerCalled = new AtomicBoolean(false); + + LatchedActionListener listener = new LatchedActionListener<>(ActionListener.wrap(r -> { + assertTrue("listener called more than once", listenerCalled.compareAndSet(false, true)); + if (onAnswer == null) { + fail("expected an exception but got a response"); + } else { + onAnswer.accept(r); + } + }, e -> { + assertTrue("listener called more than once", listenerCalled.compareAndSet(false, true)); + if (onException == null) { + logger.error("got unexpected exception", e); + fail("got unexpected exception: " + e.getMessage()); + } else if (e instanceof ValidationException) { + onException.accept((ValidationException) e); + } else { + fail("got unexpected exception type: " + e); + } + }), latch); + + function.accept(listener); + assertTrue("timed out after 20s", latch.await(20, TimeUnit.SECONDS)); + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/preview_transforms.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/preview_transforms.yml index 35289c2bbd09d..cf139b04b44c5 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/preview_transforms.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/preview_transforms.yml @@ -166,7 +166,7 @@ setup: --- "Test preview with non-existing source index": - do: - catch: /Source index \[does_not_exist\] does not exist/ + catch: /.*reason=Validation Failed.* no such index \[does_not_exist\]/ transform.preview_transform: body: > { diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/transforms_crud.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/transforms_crud.yml index fd9f75735e26c..1968e1fa431cc 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/transforms_crud.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/transforms_crud.yml @@ -79,7 +79,7 @@ setup: --- "Test put transform with invalid source index": - do: - catch: /Source index \[missing-index\] does not exist/ + catch: /.*reason=Validation Failed.* no such index \[missing-index\]/ transform.put_transform: transform_id: "missing-source-transform" body: > @@ -384,7 +384,7 @@ setup: name: source-index - do: - catch: /Destination index \[created-destination-index\] is included in source expression \[airline-data,created-destination-index\]/ + catch: /.*reason=Validation Failed.* Destination index \[created-destination-index\] is included in source expression \[airline-data,created-destination-index\]/ transform.put_transform: transform_id: "transform-from-aliases-failures" body: > @@ -410,7 +410,7 @@ setup: name: dest-index - do: - catch: /Destination index \[dest-index\] should refer to a single index/ + catch: /.*reason=Validation Failed.* no write index is defined for alias [dest2-index].*/ transform.put_transform: transform_id: "airline-transform" body: > @@ -521,7 +521,7 @@ setup: --- "Test invalid destination index name": - do: - catch: /dest\.index \[DeStInAtIoN\] must be lowercase/ + catch: /.*reason=Validation Failed.* Destination index \[DeStInAtIoN\] must be lowercase/ transform.put_transform: transform_id: "airline-transform" body: > diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/transforms_update.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/transforms_update.yml index 5b054a27fa38a..f55fcd2cb07e5 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/transforms_update.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/transform/transforms_update.yml @@ -67,7 +67,7 @@ setup: --- "Test put transform with invalid source index": - do: - catch: /Source index \[missing-index\] does not exist/ + catch: /.*reason=Validation Failed.* no such index \[missing-index\]/ transform.update_transform: transform_id: "updating-airline-transform" body: > @@ -255,7 +255,7 @@ setup: name: source2-index - do: - catch: /Destination index \[created-destination-index\] is included in source expression \[created-destination-index\]/ + catch: /.*reason=Validation Failed.* Destination index \[created-destination-index\] is included in source expression \[created-destination-index\]/ transform.update_transform: transform_id: "updating-airline-transform" body: > @@ -280,7 +280,7 @@ setup: index: created-destination-index name: dest2-index - do: - catch: /Destination index \[dest2-index\] should refer to a single index/ + catch: /.*reason=Validation Failed.* no write index is defined for alias [dest2-index].*/ transform.update_transform: transform_id: "updating-airline-transform" body: > @@ -290,7 +290,7 @@ setup: --- "Test invalid destination index name": - do: - catch: /dest\.index \[DeStInAtIoN\] must be lowercase/ + catch: /.*reason=Validation Failed.* Destination index \[DeStInAtIoN\] must be lowercase/ transform.update_transform: transform_id: "updating-airline-transform" body: > @@ -298,7 +298,7 @@ setup: "dest": { "index": "DeStInAtIoN" } } - do: - catch: /Invalid index name \[destination#dest\], must not contain \'#\'/ + catch: /.*reason=Validation Failed.* Invalid index name \[destination#dest\], must not contain \'#\'/ transform.update_transform: transform_id: "updating-airline-transform" body: > diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPreviewTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPreviewTransformAction.java index 7ee6ab5a05e0f..5ae62d0890ea9 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPreviewTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPreviewTransformAction.java @@ -16,29 +16,33 @@ import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; -import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.RemoteClusterLicenseChecker; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteClusterService; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.XPackField; +import org.elasticsearch.xpack.core.common.validation.SourceDestValidator; import org.elasticsearch.xpack.core.transform.TransformField; import org.elasticsearch.xpack.core.transform.TransformMessages; import org.elasticsearch.xpack.core.transform.action.PreviewTransformAction; @@ -65,34 +69,62 @@ public class TransportPreviewTransformAction extends private final XPackLicenseState licenseState; private final Client client; private final ThreadPool threadPool; - private final IndexNameExpressionResolver indexNameExpressionResolver; private final ClusterService clusterService; + private final SourceDestValidator sourceDestValidator; @Inject - public TransportPreviewTransformAction(TransportService transportService, ActionFilters actionFilters, - Client client, ThreadPool threadPool, XPackLicenseState licenseState, - IndexNameExpressionResolver indexNameExpressionResolver, - ClusterService clusterService) { - this(PreviewTransformAction.NAME,transportService, actionFilters, client, threadPool, licenseState, indexNameExpressionResolver, - clusterService); + public TransportPreviewTransformAction( + TransportService transportService, + ActionFilters actionFilters, + Client client, + ThreadPool threadPool, + XPackLicenseState licenseState, + IndexNameExpressionResolver indexNameExpressionResolver, + ClusterService clusterService, + Settings settings + ) { + this( + PreviewTransformAction.NAME, + transportService, + actionFilters, + client, + threadPool, + licenseState, + indexNameExpressionResolver, + clusterService, + settings + ); } - protected TransportPreviewTransformAction(String name, TransportService transportService, ActionFilters actionFilters, - Client client, ThreadPool threadPool, XPackLicenseState licenseState, - IndexNameExpressionResolver indexNameExpressionResolver, - ClusterService clusterService) { + protected TransportPreviewTransformAction( + String name, + TransportService transportService, + ActionFilters actionFilters, + Client client, + ThreadPool threadPool, + XPackLicenseState licenseState, + IndexNameExpressionResolver indexNameExpressionResolver, + ClusterService clusterService, + Settings settings + ) { super(name, transportService, actionFilters, PreviewTransformAction.Request::new); this.licenseState = licenseState; this.client = client; this.threadPool = threadPool; this.clusterService = clusterService; - this.indexNameExpressionResolver = indexNameExpressionResolver; + this.sourceDestValidator = new SourceDestValidator( + indexNameExpressionResolver, + transportService.getRemoteClusterService(), + RemoteClusterService.ENABLE_REMOTE_CLUSTERS.get(settings) + ? new RemoteClusterLicenseChecker(client, XPackLicenseState::isTransformAllowedForOperationMode) + : null, + clusterService.getNodeName(), + License.OperationMode.BASIC.description() + ); } @Override - protected void doExecute(Task task, - PreviewTransformAction.Request request, - ActionListener listener) { + protected void doExecute(Task task, PreviewTransformAction.Request request, ActionListener listener) { if (!licenseState.isTransformAllowed()) { listener.onFailure(LicenseUtils.newComplianceException(XPackField.TRANSFORM)); return; @@ -101,120 +133,121 @@ protected void doExecute(Task task, ClusterState clusterState = clusterService.state(); final TransformConfig config = request.getConfig(); - for(String src : config.getSource().getIndex()) { - String[] concreteNames = indexNameExpressionResolver.concreteIndexNames(clusterState, IndicesOptions.lenientExpandOpen(), src); - if (concreteNames.length == 0) { - listener.onFailure(new ElasticsearchStatusException( - TransformMessages.getMessage(TransformMessages.REST_PUT_TRANSFORM_SOURCE_INDEX_MISSING, src), - RestStatus.BAD_REQUEST)); - return; - } - } - Pivot pivot = new Pivot(config.getPivotConfig()); - try { - pivot.validateConfig(); - } catch (ElasticsearchStatusException e) { - listener.onFailure( - new ElasticsearchStatusException(TransformMessages.REST_PUT_TRANSFORM_FAILED_TO_VALIDATE_CONFIGURATION, - e.status(), - e)); - return; - } catch (Exception e) { - listener.onFailure(new ElasticsearchStatusException( - TransformMessages.REST_PUT_TRANSFORM_FAILED_TO_VALIDATE_CONFIGURATION, RestStatus.INTERNAL_SERVER_ERROR, e)); - return; - } + sourceDestValidator.validate( + clusterState, + config.getSource().getIndex(), + config.getDestination().getIndex(), + SourceDestValidator.PREVIEW_VALIDATIONS, + ActionListener.wrap(r -> { + + Pivot pivot = new Pivot(config.getPivotConfig()); + try { + pivot.validateConfig(); + } catch (ElasticsearchStatusException e) { + listener.onFailure( + new ElasticsearchStatusException( + TransformMessages.REST_PUT_TRANSFORM_FAILED_TO_VALIDATE_CONFIGURATION, + e.status(), + e + ) + ); + return; + } catch (Exception e) { + listener.onFailure( + new ElasticsearchStatusException( + TransformMessages.REST_PUT_TRANSFORM_FAILED_TO_VALIDATE_CONFIGURATION, + RestStatus.INTERNAL_SERVER_ERROR, + e + ) + ); + return; + } + + getPreview(pivot, config.getSource(), config.getDestination().getPipeline(), config.getDestination().getIndex(), listener); - getPreview(pivot, config.getSource(), config.getDestination().getPipeline(), config.getDestination().getIndex(), listener); + }, listener::onFailure) + ); } @SuppressWarnings("unchecked") - private void getPreview(Pivot pivot, - SourceConfig source, - String pipeline, - String dest, - ActionListener listener) { + private void getPreview( + Pivot pivot, + SourceConfig source, + String pipeline, + String dest, + ActionListener listener + ) { final PreviewTransformAction.Response previewResponse = new PreviewTransformAction.Response(); - ActionListener pipelineResponseActionListener = ActionListener.wrap( - simulatePipelineResponse -> { - List> response = new ArrayList<>(simulatePipelineResponse.getResults().size()); - for(var simulateDocumentResult : simulatePipelineResponse.getResults()) { - try(XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()) { - XContentBuilder content = simulateDocumentResult.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); - Map tempMap = XContentHelper.convertToMap(BytesReference.bytes(content), - true, - XContentType.JSON).v2(); - response.add((Map)XContentMapValues.extractValue("doc._source", tempMap)); - } + ActionListener pipelineResponseActionListener = ActionListener.wrap(simulatePipelineResponse -> { + List> response = new ArrayList<>(simulatePipelineResponse.getResults().size()); + for (var simulateDocumentResult : simulatePipelineResponse.getResults()) { + try (XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()) { + XContentBuilder content = simulateDocumentResult.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); + Map tempMap = XContentHelper.convertToMap(BytesReference.bytes(content), true, XContentType.JSON).v2(); + response.add((Map) XContentMapValues.extractValue("doc._source", tempMap)); } - previewResponse.setDocs(response); - listener.onResponse(previewResponse); - }, - listener::onFailure - ); - pivot.deduceMappings(client, source, ActionListener.wrap( - deducedMappings -> { - previewResponse.setMappingsFromStringMap(deducedMappings); - ClientHelper.executeWithHeadersAsync(threadPool.getThreadContext().getHeaders(), - ClientHelper.TRANSFORM_ORIGIN, - client, - SearchAction.INSTANCE, - pivot.buildSearchRequest(source, null, NUMBER_OF_PREVIEW_BUCKETS), - ActionListener.wrap( - r -> { - try { - final Aggregations aggregations = r.getAggregations(); - if (aggregations == null) { - listener.onFailure( - new ElasticsearchStatusException("Source indices have been deleted or closed.", - RestStatus.BAD_REQUEST) - ); - return; - } - final CompositeAggregation agg = aggregations.get(COMPOSITE_AGGREGATION_NAME); - TransformIndexerStats stats = new TransformIndexerStats(); - // remove all internal fields - - if (pipeline == null) { - List> results = pivot.extractResults(agg, deducedMappings, stats) - .peek(doc -> doc.keySet().removeIf(k -> k.startsWith("_"))) - .collect(Collectors.toList()); - previewResponse.setDocs(results); - listener.onResponse(previewResponse); - } else { - List> results = pivot.extractResults(agg, deducedMappings, stats) - .map(doc -> { - Map src = new HashMap<>(); - String id = (String) doc.get(TransformField.DOCUMENT_ID_FIELD); - doc.keySet().removeIf(k -> k.startsWith("_")); - src.put("_source", doc); - src.put("_id", id); - src.put("_index", dest); - return src; - }).collect(Collectors.toList()); - try (XContentBuilder builder = jsonBuilder()) { - builder.startObject(); - builder.field("docs", results); - builder.endObject(); - var pipelineRequest = new SimulatePipelineRequest(BytesReference.bytes(builder), XContentType.JSON); - pipelineRequest.setId(pipeline); - ClientHelper.executeAsyncWithOrigin(client, - ClientHelper.TRANSFORM_ORIGIN, - SimulatePipelineAction.INSTANCE, - pipelineRequest, - pipelineResponseActionListener); - } - } - } catch (AggregationResultUtils.AggregationExtractionException extractionException) { - listener.onFailure( - new ElasticsearchStatusException(extractionException.getMessage(), RestStatus.BAD_REQUEST)); + } + previewResponse.setDocs(response); + listener.onResponse(previewResponse); + }, listener::onFailure); + pivot.deduceMappings(client, source, ActionListener.wrap(deducedMappings -> { + previewResponse.setMappingsFromStringMap(deducedMappings); + ClientHelper.executeWithHeadersAsync( + threadPool.getThreadContext().getHeaders(), + ClientHelper.TRANSFORM_ORIGIN, + client, + SearchAction.INSTANCE, + pivot.buildSearchRequest(source, null, NUMBER_OF_PREVIEW_BUCKETS), + ActionListener.wrap(r -> { + try { + final Aggregations aggregations = r.getAggregations(); + if (aggregations == null) { + listener.onFailure( + new ElasticsearchStatusException("Source indices have been deleted or closed.", RestStatus.BAD_REQUEST) + ); + return; + } + final CompositeAggregation agg = aggregations.get(COMPOSITE_AGGREGATION_NAME); + TransformIndexerStats stats = new TransformIndexerStats(); + // remove all internal fields + + if (pipeline == null) { + List> results = pivot.extractResults(agg, deducedMappings, stats) + .peek(doc -> doc.keySet().removeIf(k -> k.startsWith("_"))) + .collect(Collectors.toList()); + previewResponse.setDocs(results); + listener.onResponse(previewResponse); + } else { + List> results = pivot.extractResults(agg, deducedMappings, stats).map(doc -> { + Map src = new HashMap<>(); + String id = (String) doc.get(TransformField.DOCUMENT_ID_FIELD); + doc.keySet().removeIf(k -> k.startsWith("_")); + src.put("_source", doc); + src.put("_id", id); + src.put("_index", dest); + return src; + }).collect(Collectors.toList()); + try (XContentBuilder builder = jsonBuilder()) { + builder.startObject(); + builder.field("docs", results); + builder.endObject(); + var pipelineRequest = new SimulatePipelineRequest(BytesReference.bytes(builder), XContentType.JSON); + pipelineRequest.setId(pipeline); + ClientHelper.executeAsyncWithOrigin( + client, + ClientHelper.TRANSFORM_ORIGIN, + SimulatePipelineAction.INSTANCE, + pipelineRequest, + pipelineResponseActionListener + ); } - }, - listener::onFailure - )); - }, - listener::onFailure - )); + } + } catch (AggregationResultUtils.AggregationExtractionException extractionException) { + listener.onFailure(new ElasticsearchStatusException(extractionException.getMessage(), RestStatus.BAD_REQUEST)); + } + }, listener::onFailure) + ); + }, listener::onFailure)); } } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPutTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPutTransformAction.java index 615fff1d54188..856ac77a801ab 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPutTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPutTransformAction.java @@ -26,17 +26,21 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.RemoteClusterLicenseChecker; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.persistent.PersistentTasksCustomMetaData; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteClusterService; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.common.validation.SourceDestValidator; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; @@ -51,7 +55,6 @@ import org.elasticsearch.xpack.transform.TransformServices; import org.elasticsearch.xpack.transform.notifications.TransformAuditor; import org.elasticsearch.xpack.transform.persistence.TransformConfigManager; -import org.elasticsearch.xpack.transform.transforms.SourceDestValidator; import org.elasticsearch.xpack.transform.transforms.pivot.Pivot; import java.io.IOException; @@ -70,6 +73,7 @@ public class TransportPutTransformAction extends TransportMasterNodeAction privResponseListener = ActionListener.wrap( - r -> handlePrivsResponse(username, request, r, listener), - listener::onFailure - ); + sourceDestValidator.validate( + clusterState, + config.getSource().getIndex(), + config.getDestination().getIndex(), + request.isDeferValidation() ? SourceDestValidator.NON_DEFERABLE_VALIDATIONS : SourceDestValidator.ALL_VALIDATIONS, + ActionListener.wrap( + validationResponse -> { + // Early check to verify that the user can create the destination index and can read from the source + if (licenseState.isAuthAllowed() && request.isDeferValidation() == false) { + final String username = securityContext.getUser().principal(); + HasPrivilegesRequest privRequest = buildPrivilegeCheck(config, indexNameExpressionResolver, clusterState, username); + ActionListener privResponseListener = ActionListener.wrap( + r -> handlePrivsResponse(username, request, r, listener), + listener::onFailure + ); - client.execute(HasPrivilegesAction.INSTANCE, privRequest, privResponseListener); - } else { // No security enabled, just create the transform - putTransform(request, listener); - } + client.execute(HasPrivilegesAction.INSTANCE, privRequest, privResponseListener); + } else { // No security enabled, just create the transform + putTransform(request, listener); + } + }, + listener::onFailure + ) + ); } @Override diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStartTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStartTransformAction.java index 6e9e65fd3a712..f18c0712575e7 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStartTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStartTransformAction.java @@ -24,17 +24,22 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.RemoteClusterLicenseChecker; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.persistent.PersistentTasksCustomMetaData; import org.elasticsearch.persistent.PersistentTasksService; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteClusterService; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.XPackField; +import org.elasticsearch.xpack.core.common.validation.SourceDestValidator; import org.elasticsearch.xpack.core.transform.TransformMessages; import org.elasticsearch.xpack.core.transform.action.StartTransformAction; import org.elasticsearch.xpack.core.transform.transforms.TransformConfig; @@ -45,7 +50,6 @@ import org.elasticsearch.xpack.transform.notifications.TransformAuditor; import org.elasticsearch.xpack.transform.persistence.TransformConfigManager; import org.elasticsearch.xpack.transform.persistence.TransformIndex; -import org.elasticsearch.xpack.transform.transforms.SourceDestValidator; import org.elasticsearch.xpack.transform.transforms.pivot.Pivot; import java.io.IOException; @@ -66,6 +70,7 @@ public class TransportStartTransformAction extends TransportMasterNodeAction transformTaskHolder = new AtomicReference<>(); + final AtomicReference transformConfigHolder = new AtomicReference<>(); - // <4> Wait for the allocated task's state to STARTED + // <5> Wait for the allocated task's state to STARTED ActionListener> newPersistentTaskActionListener = ActionListener .wrap(task -> { TransformTaskParams transformTask = transformTaskHolder.get(); @@ -157,7 +175,7 @@ protected void masterOperation( ); }, listener::onFailure); - // <3> Create the task in cluster state so that it will start executing on the node + // <4> Create the task in cluster state so that it will start executing on the node ActionListener createOrGetIndexListener = ActionListener.wrap(unused -> { TransformTaskParams transformTask = transformTaskHolder.get(); assert transformTask != null; @@ -194,26 +212,13 @@ protected void masterOperation( }, listener::onFailure); // <2> If the destination index exists, start the task, otherwise deduce our mappings for the destination index and create it - ActionListener getTransformListener = ActionListener.wrap(config -> { - if (config.isValid() == false) { - listener.onFailure( - new ElasticsearchStatusException( - TransformMessages.getMessage(TransformMessages.TRANSFORM_CONFIG_INVALID, request.getId()), - RestStatus.BAD_REQUEST - ) - ); - return; - } - // Validate source and destination indices - SourceDestValidator.validate(config, clusterService.state(), indexNameExpressionResolver, false); - - transformTaskHolder.set(createTransform(config.getId(), config.getVersion(), config.getFrequency())); - final String destinationIndex = config.getDestination().getIndex(); + ActionListener validationListener = ActionListener.wrap(validationResponse -> { + final String destinationIndex = transformConfigHolder.get().getDestination().getIndex(); String[] dest = indexNameExpressionResolver.concreteIndexNames(state, IndicesOptions.lenientExpandOpen(), destinationIndex); if (dest.length == 0) { auditor.info(request.getId(), "Creating destination index [" + destinationIndex + "] with deduced mappings."); - createDestinationIndex(config, createOrGetIndexListener); + createDestinationIndex(transformConfigHolder.get(), createOrGetIndexListener); } else { auditor.info(request.getId(), "Using existing destination index [" + destinationIndex + "]."); ClientHelper.executeAsyncWithOrigin( @@ -240,6 +245,29 @@ protected void masterOperation( } }, listener::onFailure); + // <2> run transform validations + ActionListener getTransformListener = ActionListener.wrap(config -> { + if (config.isValid() == false) { + listener.onFailure( + new ElasticsearchStatusException( + TransformMessages.getMessage(TransformMessages.TRANSFORM_CONFIG_INVALID, request.getId()), + RestStatus.BAD_REQUEST + ) + ); + return; + } + transformTaskHolder.set(createTransform(config.getId(), config.getVersion(), config.getFrequency())); + transformConfigHolder.set(config); + + sourceDestValidator.validate( + clusterService.state(), + config.getSource().getIndex(), + config.getDestination().getIndex(), + SourceDestValidator.ALL_VALIDATIONS, + validationListener + ); + }, listener::onFailure); + // <1> Get the config to verify it exists and is valid transformConfigManager.getTransformConfiguration(request.getId(), getTransformListener); } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportUpdateTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportUpdateTransformAction.java index 81526bafc933c..e98360fb453ca 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportUpdateTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportUpdateTransformAction.java @@ -23,17 +23,21 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.RemoteClusterLicenseChecker; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.persistent.PersistentTasksCustomMetaData; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteClusterService; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.common.validation.SourceDestValidator; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; @@ -51,7 +55,6 @@ import org.elasticsearch.xpack.transform.persistence.SeqNoPrimaryTermAndIndex; import org.elasticsearch.xpack.transform.persistence.TransformConfigManager; import org.elasticsearch.xpack.transform.persistence.TransformIndex; -import org.elasticsearch.xpack.transform.transforms.SourceDestValidator; import org.elasticsearch.xpack.transform.transforms.pivot.Pivot; import java.io.IOException; @@ -70,6 +73,7 @@ public class TransportUpdateTransformAction extends TransportMasterNodeAction { + checkPriviledgesAndUpdateTransform(request, clusterState, updatedConfig, configAndVersion.v2(), listener); + }, + listener::onFailure + ) + ); + }, listener::onFailure)); } @@ -197,20 +222,13 @@ private void handlePrivsResponse( } } - private void validateAndUpdateTransform( + private void checkPriviledgesAndUpdateTransform( Request request, ClusterState clusterState, TransformConfig config, SeqNoPrimaryTermAndIndex seqNoPrimaryTermAndIndex, ActionListener listener ) { - try { - SourceDestValidator.validate(config, clusterState, indexNameExpressionResolver, request.isDeferValidation()); - } catch (ElasticsearchStatusException ex) { - listener.onFailure(ex); - return; - } - // Early check to verify that the user can create the destination index and can read from the source if (licenseState.isAuthAllowed() && request.isDeferValidation() == false) { final String username = securityContext.getUser().principal(); diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/compat/TransportPreviewTransformActionDeprecated.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/compat/TransportPreviewTransformActionDeprecated.java index 8882fcf505b8b..d1ad8ac09f7f1 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/compat/TransportPreviewTransformActionDeprecated.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/compat/TransportPreviewTransformActionDeprecated.java @@ -11,6 +11,7 @@ import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -20,12 +21,27 @@ public class TransportPreviewTransformActionDeprecated extends TransportPreviewTransformAction { @Inject - public TransportPreviewTransformActionDeprecated(TransportService transportService, ActionFilters actionFilters, - Client client, ThreadPool threadPool, XPackLicenseState licenseState, - IndexNameExpressionResolver indexNameExpressionResolver, - ClusterService clusterService) { - super(PreviewTransformActionDeprecated.NAME, transportService, actionFilters, client, threadPool, licenseState, - indexNameExpressionResolver, clusterService); + public TransportPreviewTransformActionDeprecated( + TransportService transportService, + ActionFilters actionFilters, + Client client, + ThreadPool threadPool, + XPackLicenseState licenseState, + IndexNameExpressionResolver indexNameExpressionResolver, + ClusterService clusterService, + Settings settings + ) { + super( + PreviewTransformActionDeprecated.NAME, + transportService, + actionFilters, + client, + threadPool, + licenseState, + indexNameExpressionResolver, + clusterService, + settings + ); } } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/compat/TransportStartTransformActionDeprecated.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/compat/TransportStartTransformActionDeprecated.java index 17a996f760eb4..caef1b1edf1d3 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/compat/TransportStartTransformActionDeprecated.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/compat/TransportStartTransformActionDeprecated.java @@ -11,6 +11,7 @@ import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.persistent.PersistentTasksService; import org.elasticsearch.threadpool.ThreadPool; @@ -31,7 +32,8 @@ public TransportStartTransformActionDeprecated( IndexNameExpressionResolver indexNameExpressionResolver, TransformServices transformServices, PersistentTasksService persistentTasksService, - Client client + Client client, + Settings settings ) { super( StartTransformActionDeprecated.NAME, @@ -43,7 +45,8 @@ public TransportStartTransformActionDeprecated( indexNameExpressionResolver, transformServices, persistentTasksService, - client + client, + settings ); } } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/SourceDestValidator.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/SourceDestValidator.java deleted file mode 100644 index 8c89ffd6d5f55..0000000000000 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/SourceDestValidator.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.transform.transforms; - -import org.elasticsearch.ElasticsearchStatusException; -import org.elasticsearch.action.support.IndicesOptions; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.regex.Regex; -import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.xpack.core.transform.TransformMessages; -import org.elasticsearch.xpack.core.transform.transforms.TransformConfig; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * This class contains more complex validations in regards to how {@link TransformConfig#getSource()} and - * {@link TransformConfig#getDestination()} relate to each other. - */ -public final class SourceDestValidator { - - interface SourceDestValidation { - boolean isDeferrable(); - void validate(TransformConfig config, ClusterState clusterState, IndexNameExpressionResolver indexNameExpressionResolver); - } - - private static final List VALIDATIONS = Arrays.asList(new SourceMissingValidation(), - new DestinationInSourceValidation(), - new DestinationSingleIndexValidation()); - - /** - * Validates the DataFrameTransformConfiguration source and destination indices. - * - * A simple name validation is done on {@link TransformConfig#getDestination()} inside - * {@link org.elasticsearch.xpack.core.transform.action.PutTransformAction} - * - * So, no need to do the name checks here. - * - * @param config DataFrameTransformConfig to validate - * @param clusterState The current ClusterState - * @param indexNameExpressionResolver A valid IndexNameExpressionResolver object - * @throws ElasticsearchStatusException when a validation fails - */ - public static void validate(TransformConfig config, - ClusterState clusterState, - IndexNameExpressionResolver indexNameExpressionResolver, - boolean shouldDefer) { - for (SourceDestValidation validation : VALIDATIONS) { - if (shouldDefer && validation.isDeferrable()) { - continue; - } - validation.validate(config, clusterState, indexNameExpressionResolver); - } - } - - static class SourceMissingValidation implements SourceDestValidation { - - @Override - public boolean isDeferrable() { - return true; - } - - @Override - public void validate(TransformConfig config, - ClusterState clusterState, - IndexNameExpressionResolver indexNameExpressionResolver) { - for(String src : config.getSource().getIndex()) { - String[] concreteNames = indexNameExpressionResolver.concreteIndexNames(clusterState, - IndicesOptions.lenientExpandOpen(), - src); - if (concreteNames.length == 0) { - throw new ElasticsearchStatusException( - TransformMessages.getMessage(TransformMessages.REST_PUT_TRANSFORM_SOURCE_INDEX_MISSING, src), - RestStatus.BAD_REQUEST); - } - } - } - } - - static class DestinationInSourceValidation implements SourceDestValidation { - - @Override - public boolean isDeferrable() { - return true; - } - - @Override - public void validate(TransformConfig config, - ClusterState clusterState, - IndexNameExpressionResolver indexNameExpressionResolver) { - final String destIndex = config.getDestination().getIndex(); - Set concreteSourceIndexNames = new HashSet<>(); - for(String src : config.getSource().getIndex()) { - String[] concreteNames = indexNameExpressionResolver.concreteIndexNames(clusterState, - IndicesOptions.lenientExpandOpen(), - src); - if (Regex.simpleMatch(src, destIndex)) { - throw new ElasticsearchStatusException( - TransformMessages.getMessage(TransformMessages.REST_PUT_TRANSFORM_DEST_IN_SOURCE, destIndex, src), - RestStatus.BAD_REQUEST); - } - concreteSourceIndexNames.addAll(Arrays.asList(concreteNames)); - } - - if (concreteSourceIndexNames.contains(destIndex)) { - throw new ElasticsearchStatusException( - TransformMessages.getMessage(TransformMessages.REST_PUT_TRANSFORM_DEST_IN_SOURCE, - destIndex, - Strings.arrayToCommaDelimitedString(config.getSource().getIndex())), - RestStatus.BAD_REQUEST - ); - } - - final String[] concreteDest = indexNameExpressionResolver.concreteIndexNames(clusterState, - IndicesOptions.lenientExpandOpen(), - destIndex); - if (concreteDest.length > 0 && concreteSourceIndexNames.contains(concreteDest[0])) { - throw new ElasticsearchStatusException( - TransformMessages.getMessage(TransformMessages.REST_PUT_TRANSFORM_DEST_IN_SOURCE, - concreteDest[0], - Strings.arrayToCommaDelimitedString(concreteSourceIndexNames.toArray(new String[0]))), - RestStatus.BAD_REQUEST - ); - } - } - } - - static class DestinationSingleIndexValidation implements SourceDestValidation { - - @Override - public boolean isDeferrable() { - return false; - } - - @Override - public void validate(TransformConfig config, - ClusterState clusterState, - IndexNameExpressionResolver indexNameExpressionResolver) { - final String destIndex = config.getDestination().getIndex(); - final String[] concreteDest = - indexNameExpressionResolver.concreteIndexNames(clusterState, IndicesOptions.lenientExpandOpen(), destIndex); - - if (concreteDest.length > 1) { - throw new ElasticsearchStatusException( - TransformMessages.getMessage(TransformMessages.REST_PUT_TRANSFORM_DEST_SINGLE_INDEX, destIndex), - RestStatus.BAD_REQUEST - ); - } - } - } -} diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/SourceDestValidatorTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/SourceDestValidatorTests.java deleted file mode 100644 index add6a0c27b908..0000000000000 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/SourceDestValidatorTests.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.transform.transforms; - -import org.elasticsearch.ElasticsearchStatusException; -import org.elasticsearch.Version; -import org.elasticsearch.cluster.ClusterName; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.metadata.AliasMetaData; -import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; -import org.elasticsearch.cluster.metadata.MetaData; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.core.transform.transforms.TransformConfig; -import org.elasticsearch.xpack.core.transform.transforms.DestConfig; -import org.elasticsearch.xpack.core.transform.transforms.SourceConfig; -import org.elasticsearch.xpack.core.transform.transforms.pivot.PivotConfigTests; - -import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_CREATION_DATE; -import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS; -import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS; -import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_VERSION_CREATED; -import static org.hamcrest.Matchers.equalTo; - -public class SourceDestValidatorTests extends ESTestCase { - - private static final String SOURCE_1 = "source-1"; - private static final String SOURCE_2 = "source-2"; - private static final String ALIASED_DEST = "aliased-dest"; - - private static final ClusterState CLUSTER_STATE; - - static { - IndexMetaData source1 = IndexMetaData.builder(SOURCE_1).settings(Settings.builder() - .put(SETTING_VERSION_CREATED, Version.CURRENT) - .put(SETTING_NUMBER_OF_SHARDS, 1).put(SETTING_NUMBER_OF_REPLICAS, 0) - .put(SETTING_CREATION_DATE, System.currentTimeMillis())) - .putAlias(AliasMetaData.builder("source-1-alias").build()) - .build(); - IndexMetaData source2 = IndexMetaData.builder(SOURCE_2).settings(Settings.builder() - .put(SETTING_VERSION_CREATED, Version.CURRENT) - .put(SETTING_NUMBER_OF_SHARDS, 1).put(SETTING_NUMBER_OF_REPLICAS, 0) - .put(SETTING_CREATION_DATE, System.currentTimeMillis())) - .putAlias(AliasMetaData.builder("dest-alias").build()) - .build(); - IndexMetaData aliasedDest = IndexMetaData.builder(ALIASED_DEST).settings(Settings.builder() - .put(SETTING_VERSION_CREATED, Version.CURRENT) - .put(SETTING_NUMBER_OF_SHARDS, 1).put(SETTING_NUMBER_OF_REPLICAS, 0) - .put(SETTING_CREATION_DATE, System.currentTimeMillis())) - .putAlias(AliasMetaData.builder("dest-alias").build()) - .build(); - ClusterState.Builder state = ClusterState.builder(new ClusterName("test")); - state.metaData(MetaData.builder() - .put(IndexMetaData.builder(source1).build(), false) - .put(IndexMetaData.builder(source2).build(), false) - .put(IndexMetaData.builder(aliasedDest).build(), false)); - CLUSTER_STATE = state.build(); - } - - public void testCheck_GivenSimpleSourceIndexAndValidDestIndex() { - TransformConfig config = createTransform(new SourceConfig(SOURCE_1), new DestConfig("dest", null)); - SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), false); - } - - public void testCheck_GivenMissingConcreteSourceIndex() { - TransformConfig config = createTransform(new SourceConfig("missing"), new DestConfig("dest", null)); - - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), false)); - assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); - assertThat(e.getMessage(), equalTo("Source index [missing] does not exist")); - SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), true); - } - - public void testCheck_GivenMissingWildcardSourceIndex() { - TransformConfig config = createTransform(new SourceConfig("missing*"), new DestConfig("dest", null)); - - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), false)); - assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); - assertThat(e.getMessage(), equalTo("Source index [missing*] does not exist")); - SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), true); - } - - public void testCheck_GivenDestIndexSameAsSourceIndex() { - TransformConfig config = createTransform(new SourceConfig(SOURCE_1), new DestConfig("source-1", null)); - - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), false)); - assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); - assertThat(e.getMessage(), equalTo("Destination index [source-1] is included in source expression [source-1]")); - SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), true); - } - - public void testCheck_GivenDestIndexMatchesSourceIndex() { - TransformConfig config = createTransform(new SourceConfig("source-*"), new DestConfig(SOURCE_2, null)); - - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), false)); - assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); - assertThat(e.getMessage(), equalTo("Destination index [source-2] is included in source expression [source-*]")); - SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), true); - } - - public void testCheck_GivenDestIndexMatchesOneOfSourceIndices() { - TransformConfig config = createTransform(new SourceConfig("source-1", "source-*"), - new DestConfig(SOURCE_2, null)); - - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), false)); - assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); - assertThat(e.getMessage(), equalTo("Destination index [source-2] is included in source expression [source-*]")); - SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), true); - } - - public void testCheck_GivenDestIndexIsAliasThatMatchesMultipleIndices() { - TransformConfig config = createTransform(new SourceConfig(SOURCE_1), new DestConfig("dest-alias", null)); - - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), false)); - assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); - assertThat(e.getMessage(), - equalTo("Destination index [dest-alias] should refer to a single index")); - - e = expectThrows(ElasticsearchStatusException.class, - () -> SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), true)); - assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); - assertThat(e.getMessage(), - equalTo("Destination index [dest-alias] should refer to a single index")); - } - - public void testCheck_GivenDestIndexIsAliasThatIsIncludedInSource() { - TransformConfig config = createTransform(new SourceConfig(SOURCE_1), new DestConfig("source-1-alias", null)); - - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, - () -> SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), false)); - assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); - assertThat(e.getMessage(), - equalTo("Destination index [source-1] is included in source expression [source-1]")); - - SourceDestValidator.validate(config, CLUSTER_STATE, new IndexNameExpressionResolver(), true); - } - - private static TransformConfig createTransform(SourceConfig sourceConfig, DestConfig destConfig) { - return new TransformConfig("test", - sourceConfig, - destConfig, - TimeValue.timeValueSeconds(60), - null, - null, - PivotConfigTests.randomPivotConfig(), - null); - } -} From 822dec64daeac4fb96443e700267ca5762684363 Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Fri, 20 Dec 2019 08:28:16 +0100 Subject: [PATCH 293/686] [Transform] improve checkpoint reporting (#50369) fixes empty checkpoints, re-factors checkpoint info creation (moves builder) and always reports last change detection relates #43201 relates #50018 --- .../transforms/TransformCheckpoint.java | 45 ++-- .../TransformCheckpointingInfo.java | 193 ++++++++++++++---- .../transforms/TransformCheckpointTests.java | 87 +++++--- .../TransportGetTransformStatsAction.java | 3 +- .../checkpoint/CheckpointProvider.java | 26 ++- .../checkpoint/DefaultCheckpointProvider.java | 107 ++-------- .../TransformCheckpointService.java | 4 +- .../transform/transforms/TransformTask.java | 24 ++- .../TransformCheckpointServiceNodeTests.java | 46 ++++- 9 files changed, 325 insertions(+), 210 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpoint.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpoint.java index 8431abc886d85..da5c4e934a33a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpoint.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpoint.java @@ -62,20 +62,19 @@ public class TransformCheckpoint implements Writeable, ToXContentObject { private final long timeUpperBoundMillis; private static ConstructingObjectParser createParser(boolean lenient) { - ConstructingObjectParser parser = new ConstructingObjectParser<>(NAME, - lenient, args -> { - String id = (String) args[0]; - long timestamp = (Long) args[1]; - long checkpoint = (Long) args[2]; + ConstructingObjectParser parser = new ConstructingObjectParser<>(NAME, lenient, args -> { + String id = (String) args[0]; + long timestamp = (Long) args[1]; + long checkpoint = (Long) args[2]; - @SuppressWarnings("unchecked") - Map checkpoints = (Map) args[3]; + @SuppressWarnings("unchecked") + Map checkpoints = (Map) args[3]; - Long timeUpperBound = (Long) args[4]; + Long timeUpperBound = (Long) args[4]; - // ignored, only for internal storage: String docType = (String) args[5]; - return new TransformCheckpoint(id, timestamp, checkpoint, checkpoints, timeUpperBound); - }); + // ignored, only for internal storage: String docType = (String) args[5]; + return new TransformCheckpoint(id, timestamp, checkpoint, checkpoints, timeUpperBound); + }); parser.declareString(constructorArg(), TransformField.ID); @@ -83,7 +82,7 @@ private static ConstructingObjectParser createParser( parser.declareLong(constructorArg(), TransformField.TIMESTAMP_MILLIS); parser.declareLong(constructorArg(), CHECKPOINT); - parser.declareObject(constructorArg(), (p,c) -> { + parser.declareObject(constructorArg(), (p, c) -> { Map checkPointsByIndexName = new TreeMap<>(); XContentParser.Token token = null; while ((token = p.nextToken()) != XContentParser.Token.END_OBJECT) { @@ -108,8 +107,7 @@ private static ConstructingObjectParser createParser( return parser; } - public TransformCheckpoint(String transformId, long timestamp, long checkpoint, Map checkpoints, - Long timeUpperBound) { + public TransformCheckpoint(String transformId, long timestamp, long checkpoint, Map checkpoints, Long timeUpperBound) { this.transformId = Objects.requireNonNull(transformId); this.timestampMillis = timestamp; this.checkpoint = checkpoint; @@ -126,7 +124,7 @@ public TransformCheckpoint(StreamInput in) throws IOException { } public boolean isEmpty() { - return indicesCheckpoints.isEmpty(); + return this.equals(EMPTY); } /** @@ -212,8 +210,10 @@ public boolean equals(Object other) { final TransformCheckpoint that = (TransformCheckpoint) other; // compare the timestamp, id, checkpoint and than call matches for the rest - return this.timestampMillis == that.timestampMillis && this.checkpoint == that.checkpoint - && this.timeUpperBoundMillis == that.timeUpperBoundMillis && matches(that); + return this.timestampMillis == that.timestampMillis + && this.checkpoint == that.checkpoint + && this.timeUpperBoundMillis == that.timeUpperBoundMillis + && matches(that); } /** @@ -224,7 +224,7 @@ public boolean equals(Object other) { * @param that other checkpoint * @return true if checkpoints match */ - public boolean matches (TransformCheckpoint that) { + public boolean matches(TransformCheckpoint that) { if (this == that) { return true; } @@ -258,7 +258,7 @@ public static String documentId(String transformId, long checkpoint) { return NAME + "-" + transformId + "-" + checkpoint; } - public static boolean isNullOrEmpty (TransformCheckpoint checkpoint) { + public static boolean isNullOrEmpty(TransformCheckpoint checkpoint) { return checkpoint == null || checkpoint.isEmpty(); } @@ -315,8 +315,11 @@ private static Map readCheckpoints(Map readMap) if (e.getValue() instanceof long[]) { checkpoints.put(e.getKey(), (long[]) e.getValue()); } else { - throw new ElasticsearchParseException("expecting the checkpoints for [{}] to be a long[], but found [{}] instead", - e.getKey(), e.getValue().getClass()); + throw new ElasticsearchParseException( + "expecting the checkpoints for [{}] to be a long[], but found [{}] instead", + e.getKey(), + e.getValue().getClass() + ); } } return checkpoints; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpointingInfo.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpointingInfo.java index b0f665b7b8075..8a2f04e1e82b0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpointingInfo.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpointingInfo.java @@ -31,11 +31,110 @@ */ public class TransformCheckpointingInfo implements Writeable, ToXContentObject { + /** + * Builder for collecting checkpointing information for the purpose of _stats + */ + public static class TransformCheckpointingInfoBuilder { + private TransformIndexerPosition nextCheckpointPosition; + private TransformProgress nextCheckpointProgress; + private TransformCheckpoint lastCheckpoint; + private TransformCheckpoint nextCheckpoint; + private TransformCheckpoint sourceCheckpoint; + private Instant changesLastDetectedAt; + private long operationsBehind; + + public TransformCheckpointingInfoBuilder() {} + + public TransformCheckpointingInfo build() { + if (lastCheckpoint == null) { + lastCheckpoint = TransformCheckpoint.EMPTY; + } + if (nextCheckpoint == null) { + nextCheckpoint = TransformCheckpoint.EMPTY; + } + if (sourceCheckpoint == null) { + sourceCheckpoint = TransformCheckpoint.EMPTY; + } + + // checkpointstats requires a non-negative checkpoint number + long lastCheckpointNumber = lastCheckpoint.getCheckpoint() > 0 ? lastCheckpoint.getCheckpoint() : 0; + long nextCheckpointNumber = nextCheckpoint.getCheckpoint() > 0 ? nextCheckpoint.getCheckpoint() : 0; + + return new TransformCheckpointingInfo( + new TransformCheckpointStats( + lastCheckpointNumber, + null, + null, + lastCheckpoint.getTimestamp(), + lastCheckpoint.getTimeUpperBound() + ), + new TransformCheckpointStats( + nextCheckpointNumber, + nextCheckpointPosition, + nextCheckpointProgress, + nextCheckpoint.getTimestamp(), + nextCheckpoint.getTimeUpperBound() + ), + operationsBehind, + changesLastDetectedAt + ); + } + + public TransformCheckpointingInfoBuilder setLastCheckpoint(TransformCheckpoint lastCheckpoint) { + this.lastCheckpoint = lastCheckpoint; + return this; + } + + public TransformCheckpoint getLastCheckpoint() { + return lastCheckpoint; + } + + public TransformCheckpointingInfoBuilder setNextCheckpoint(TransformCheckpoint nextCheckpoint) { + this.nextCheckpoint = nextCheckpoint; + return this; + } + + public TransformCheckpoint getNextCheckpoint() { + return nextCheckpoint; + } + + public TransformCheckpointingInfoBuilder setSourceCheckpoint(TransformCheckpoint sourceCheckpoint) { + this.sourceCheckpoint = sourceCheckpoint; + return this; + } + + public TransformCheckpoint getSourceCheckpoint() { + return sourceCheckpoint; + } + + public TransformCheckpointingInfoBuilder setNextCheckpointProgress(TransformProgress nextCheckpointProgress) { + this.nextCheckpointProgress = nextCheckpointProgress; + return this; + } + + public TransformCheckpointingInfoBuilder setNextCheckpointPosition(TransformIndexerPosition nextCheckpointPosition) { + this.nextCheckpointPosition = nextCheckpointPosition; + return this; + } + + public TransformCheckpointingInfoBuilder setChangesLastDetectedAt(Instant changesLastDetectedAt) { + this.changesLastDetectedAt = changesLastDetectedAt; + return this; + } + + public TransformCheckpointingInfoBuilder setOperationsBehind(long operationsBehind) { + this.operationsBehind = operationsBehind; + return this; + } + + } + public static final TransformCheckpointingInfo EMPTY = new TransformCheckpointingInfo( TransformCheckpointStats.EMPTY, TransformCheckpointStats.EMPTY, 0L, - null); + null + ); public static final ParseField LAST_CHECKPOINT = new ParseField("last"); public static final ParseField NEXT_CHECKPOINT = new ParseField("next"); @@ -44,32 +143,41 @@ public class TransformCheckpointingInfo implements Writeable, ToXContentObject { private final TransformCheckpointStats last; private final TransformCheckpointStats next; private final long operationsBehind; - private Instant changesLastDetectedAt; - - private static final ConstructingObjectParser LENIENT_PARSER = - new ConstructingObjectParser<>( - "data_frame_transform_checkpointing_info", - true, - a -> { - long behind = a[2] == null ? 0L : (Long) a[2]; - Instant changesLastDetectedAt = (Instant)a[3]; - return new TransformCheckpointingInfo( - a[0] == null ? TransformCheckpointStats.EMPTY : (TransformCheckpointStats) a[0], - a[1] == null ? TransformCheckpointStats.EMPTY : (TransformCheckpointStats) a[1], - behind, - changesLastDetectedAt); - }); + private final Instant changesLastDetectedAt; + + private static final ConstructingObjectParser LENIENT_PARSER = new ConstructingObjectParser<>( + "data_frame_transform_checkpointing_info", + true, + a -> { + long behind = a[2] == null ? 0L : (Long) a[2]; + Instant changesLastDetectedAt = (Instant) a[3]; + return new TransformCheckpointingInfo( + a[0] == null ? TransformCheckpointStats.EMPTY : (TransformCheckpointStats) a[0], + a[1] == null ? TransformCheckpointStats.EMPTY : (TransformCheckpointStats) a[1], + behind, + changesLastDetectedAt + ); + } + ); static { - LENIENT_PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), - TransformCheckpointStats.LENIENT_PARSER::apply, LAST_CHECKPOINT); - LENIENT_PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), - TransformCheckpointStats.LENIENT_PARSER::apply, NEXT_CHECKPOINT); + LENIENT_PARSER.declareObject( + ConstructingObjectParser.optionalConstructorArg(), + TransformCheckpointStats.LENIENT_PARSER::apply, + LAST_CHECKPOINT + ); + LENIENT_PARSER.declareObject( + ConstructingObjectParser.optionalConstructorArg(), + TransformCheckpointStats.LENIENT_PARSER::apply, + NEXT_CHECKPOINT + ); LENIENT_PARSER.declareLong(ConstructingObjectParser.optionalConstructorArg(), OPERATIONS_BEHIND); - LENIENT_PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), + LENIENT_PARSER.declareField( + ConstructingObjectParser.optionalConstructorArg(), p -> TimeUtils.parseTimeFieldToInstant(p, CHANGES_LAST_DETECTED_AT.getPreferredName()), CHANGES_LAST_DETECTED_AT, - ObjectParser.ValueType.VALUE); + ObjectParser.ValueType.VALUE + ); } /** @@ -81,28 +189,26 @@ public class TransformCheckpointingInfo implements Writeable, ToXContentObject { * @param operationsBehind counter of operations the current checkpoint is behind source * @param changesLastDetectedAt the last time the source indices were checked for changes */ - public TransformCheckpointingInfo(TransformCheckpointStats last, - TransformCheckpointStats next, - long operationsBehind, - Instant changesLastDetectedAt) { + public TransformCheckpointingInfo( + TransformCheckpointStats last, + TransformCheckpointStats next, + long operationsBehind, + Instant changesLastDetectedAt + ) { this.last = Objects.requireNonNull(last); this.next = Objects.requireNonNull(next); this.operationsBehind = operationsBehind; this.changesLastDetectedAt = changesLastDetectedAt == null ? null : Instant.ofEpochMilli(changesLastDetectedAt.toEpochMilli()); } - public TransformCheckpointingInfo(TransformCheckpointStats last, - TransformCheckpointStats next, - long operationsBehind) { - this(last, next, operationsBehind, null); - } - public TransformCheckpointingInfo(StreamInput in) throws IOException { last = new TransformCheckpointStats(in); next = new TransformCheckpointStats(in); operationsBehind = in.readLong(); if (in.getVersion().onOrAfter(Version.V_7_4_0)) { changesLastDetectedAt = in.readOptionalInstant(); + } else { + changesLastDetectedAt = null; } } @@ -122,11 +228,6 @@ public Instant getChangesLastDetectedAt() { return changesLastDetectedAt; } - public TransformCheckpointingInfo setChangesLastDetectedAt(Instant changesLastDetectedAt) { - this.changesLastDetectedAt = Instant.ofEpochMilli(Objects.requireNonNull(changesLastDetectedAt).toEpochMilli()); - return this; - } - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -134,11 +235,15 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (next.getCheckpoint() > 0) { builder.field(NEXT_CHECKPOINT.getPreferredName(), next); } - builder.field(OPERATIONS_BEHIND.getPreferredName(), operationsBehind); + if (operationsBehind > 0) { + builder.field(OPERATIONS_BEHIND.getPreferredName(), operationsBehind); + } if (changesLastDetectedAt != null) { - builder.timeField(CHANGES_LAST_DETECTED_AT.getPreferredName(), + builder.timeField( + CHANGES_LAST_DETECTED_AT.getPreferredName(), CHANGES_LAST_DETECTED_AT.getPreferredName() + "_string", - changesLastDetectedAt.toEpochMilli()); + changesLastDetectedAt.toEpochMilli() + ); } builder.endObject(); return builder; @@ -175,10 +280,10 @@ public boolean equals(Object other) { TransformCheckpointingInfo that = (TransformCheckpointingInfo) other; - return Objects.equals(this.last, that.last) && - Objects.equals(this.next, that.next) && - this.operationsBehind == that.operationsBehind && - Objects.equals(this.changesLastDetectedAt, that.changesLastDetectedAt); + return Objects.equals(this.last, that.last) + && Objects.equals(this.next, that.next) + && this.operationsBehind == that.operationsBehind + && Objects.equals(this.changesLastDetectedAt, that.changesLastDetectedAt); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpointTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpointTests.java index f70f6c68e0179..86955d12cdbba 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpointTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformCheckpointTests.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -23,8 +24,13 @@ public class TransformCheckpointTests extends AbstractSerializingTransformTestCase { public static TransformCheckpoint randomTransformCheckpoints() { - return new TransformCheckpoint(randomAlphaOfLengthBetween(1, 10), randomNonNegativeLong(), randomNonNegativeLong(), - randomCheckpointsByIndex(), randomNonNegativeLong()); + return new TransformCheckpoint( + randomAlphaOfLengthBetween(1, 10), + randomNonNegativeLong(), + randomNonNegativeLong(), + randomCheckpointsByIndex(), + randomNonNegativeLong() + ); } @Override @@ -62,8 +68,13 @@ public void testMatches() throws IOException { otherCheckpointsByIndex.put(randomAlphaOfLengthBetween(1, 10), new long[] { 1, 2, 3 }); long timeUpperBound = randomNonNegativeLong(); - TransformCheckpoint dataFrameTransformCheckpoints = new TransformCheckpoint(id, timestamp, checkpoint, - checkpointsByIndex, timeUpperBound); + TransformCheckpoint dataFrameTransformCheckpoints = new TransformCheckpoint( + id, + timestamp, + checkpoint, + checkpointsByIndex, + timeUpperBound + ); // same assertTrue(dataFrameTransformCheckpoints.matches(dataFrameTransformCheckpoints)); @@ -74,20 +85,40 @@ public void testMatches() throws IOException { assertTrue(dataFrameTransformCheckpointsCopy.matches(dataFrameTransformCheckpoints)); // other id - assertFalse(dataFrameTransformCheckpoints - .matches(new TransformCheckpoint(id + "-1", timestamp, checkpoint, checkpointsByIndex, timeUpperBound))); + assertFalse( + dataFrameTransformCheckpoints.matches( + new TransformCheckpoint(id + "-1", timestamp, checkpoint, checkpointsByIndex, timeUpperBound) + ) + ); // other timestamp - assertTrue(dataFrameTransformCheckpoints - .matches(new TransformCheckpoint(id, (timestamp / 2) + 1, checkpoint, checkpointsByIndex, timeUpperBound))); + assertTrue( + dataFrameTransformCheckpoints.matches( + new TransformCheckpoint(id, (timestamp / 2) + 1, checkpoint, checkpointsByIndex, timeUpperBound) + ) + ); // other checkpoint - assertTrue(dataFrameTransformCheckpoints - .matches(new TransformCheckpoint(id, timestamp, (checkpoint / 2) + 1, checkpointsByIndex, timeUpperBound))); + assertTrue( + dataFrameTransformCheckpoints.matches( + new TransformCheckpoint(id, timestamp, (checkpoint / 2) + 1, checkpointsByIndex, timeUpperBound) + ) + ); // other index checkpoints - assertFalse(dataFrameTransformCheckpoints - .matches(new TransformCheckpoint(id, timestamp, checkpoint, otherCheckpointsByIndex, timeUpperBound))); + assertFalse( + dataFrameTransformCheckpoints.matches( + new TransformCheckpoint(id, timestamp, checkpoint, otherCheckpointsByIndex, timeUpperBound) + ) + ); // other time upper bound - assertTrue(dataFrameTransformCheckpoints - .matches(new TransformCheckpoint(id, timestamp, checkpoint, checkpointsByIndex, (timeUpperBound / 2) + 1))); + assertTrue( + dataFrameTransformCheckpoints.matches( + new TransformCheckpoint(id, timestamp, checkpoint, checkpointsByIndex, (timeUpperBound / 2) + 1) + ) + ); + } + + public void testEmpty() { + assertTrue(TransformCheckpoint.EMPTY.isEmpty()); + assertFalse(new TransformCheckpoint("some_id", 0L, -1, Collections.emptyMap(), 0L).isEmpty()); } public void testGetBehind() { @@ -119,14 +150,16 @@ public void testGetBehind() { long checkpoint = randomLongBetween(10, 100); - TransformCheckpoint checkpointOld = new TransformCheckpoint( - id, timestamp, checkpoint, checkpointsByIndexOld, 0L); - TransformCheckpoint checkpointTransientNew = new TransformCheckpoint( - id, timestamp, -1L, checkpointsByIndexNew, 0L); - TransformCheckpoint checkpointNew = new TransformCheckpoint( - id, timestamp, checkpoint + 1, checkpointsByIndexNew, 0L); + TransformCheckpoint checkpointOld = new TransformCheckpoint(id, timestamp, checkpoint, checkpointsByIndexOld, 0L); + TransformCheckpoint checkpointTransientNew = new TransformCheckpoint(id, timestamp, -1L, checkpointsByIndexNew, 0L); + TransformCheckpoint checkpointNew = new TransformCheckpoint(id, timestamp, checkpoint + 1, checkpointsByIndexNew, 0L); TransformCheckpoint checkpointOlderButNewerShardsCheckpoint = new TransformCheckpoint( - id, timestamp, checkpoint - 1, checkpointsByIndexNew, 0L); + id, + timestamp, + checkpoint - 1, + checkpointsByIndexNew, + 0L + ); assertEquals(indices * shards * 10L, TransformCheckpoint.getBehind(checkpointOld, checkpointTransientNew)); assertEquals(indices * shards * 10L, TransformCheckpoint.getBehind(checkpointOld, checkpointNew)); @@ -140,8 +173,10 @@ public void testGetBehind() { assertEquals(0L, TransformCheckpoint.getBehind(checkpointNew, checkpointTransientNew)); // transient new vs new: illegal - Exception e = expectThrows(IllegalArgumentException.class, - () -> TransformCheckpoint.getBehind(checkpointTransientNew, checkpointNew)); + Exception e = expectThrows( + IllegalArgumentException.class, + () -> TransformCheckpoint.getBehind(checkpointTransientNew, checkpointNew) + ); assertEquals("can not compare transient against a non transient checkpoint", e.getMessage()); // new vs old: illegal @@ -155,8 +190,10 @@ public void testGetBehind() { // remove something from old, so newer has 1 index more than old: should be equivalent to old index existing but empty checkpointsByIndexOld.remove(checkpointsByIndexOld.firstKey()); long behind = TransformCheckpoint.getBehind(checkpointOld, checkpointTransientNew); - assertTrue("Expected behind (" + behind + ") => sum of shard checkpoint differences (" + indices * shards * 10L + ")", - behind >= indices * shards * 10L); + assertTrue( + "Expected behind (" + behind + ") => sum of shard checkpoint differences (" + indices * shards * 10L + ")", + behind >= indices * shards * 10L + ); // remove same key: old and new should have equal indices again checkpointsByIndexNew.remove(checkpointsByIndexNew.firstKey()); diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetTransformStatsAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetTransformStatsAction.java index b9dffa5d50024..f77ef92483af2 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetTransformStatsAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetTransformStatsAction.java @@ -207,7 +207,6 @@ private void collectStatsForTransformsWithoutTasks(Request request, Response res listener.onResponse(response); return; } - Set transformsWithoutTasks = new HashSet<>(request.getExpandedIds()); transformsWithoutTasks.removeAll(response.getTransformsStats().stream().map(TransformStats::getId).collect(Collectors.toList())); @@ -252,7 +251,7 @@ private void populateSingleStoppedTransformStat(TransformStoredDoc transform, Ac transform.getTransformState().getCheckpoint(), transform.getTransformState().getPosition(), transform.getTransformState().getProgress(), - ActionListener.wrap(listener::onResponse, e -> { + ActionListener.wrap(infoBuilder -> listener.onResponse(infoBuilder.build()), e -> { logger.warn("Failed to retrieve checkpointing info for transform [" + transform.getId() + "]", e); listener.onResponse(TransformCheckpointingInfo.EMPTY); }) diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/CheckpointProvider.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/CheckpointProvider.java index 5fca07cef2b0e..d8b084cbd86a0 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/CheckpointProvider.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/CheckpointProvider.java @@ -7,9 +7,9 @@ package org.elasticsearch.xpack.transform.checkpoint; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerPosition; import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpoint; -import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointingInfo; +import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointingInfo.TransformCheckpointingInfoBuilder; +import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerPosition; import org.elasticsearch.xpack.core.transform.transforms.TransformProgress; /** @@ -44,11 +44,13 @@ public interface CheckpointProvider { * @param nextCheckpointProgress progress for the next checkpoint * @param listener listener to retrieve the result */ - void getCheckpointingInfo(TransformCheckpoint lastCheckpoint, - TransformCheckpoint nextCheckpoint, - TransformIndexerPosition nextCheckpointPosition, - TransformProgress nextCheckpointProgress, - ActionListener listener); + void getCheckpointingInfo( + TransformCheckpoint lastCheckpoint, + TransformCheckpoint nextCheckpoint, + TransformIndexerPosition nextCheckpointPosition, + TransformProgress nextCheckpointProgress, + ActionListener listener + ); /** * Get checkpoint statistics for a stopped data frame @@ -60,8 +62,10 @@ void getCheckpointingInfo(TransformCheckpoint lastCheckpoint, * @param nextCheckpointProgress progress for the next checkpoint * @param listener listener to retrieve the result */ - void getCheckpointingInfo(long lastCheckpointNumber, - TransformIndexerPosition nextCheckpointPosition, - TransformProgress nextCheckpointProgress, - ActionListener listener); + void getCheckpointingInfo( + long lastCheckpointNumber, + TransformIndexerPosition nextCheckpointPosition, + TransformProgress nextCheckpointProgress, + ActionListener listener + ); } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/DefaultCheckpointProvider.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/DefaultCheckpointProvider.java index 8abb6ba159a79..a7bda6886fd58 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/DefaultCheckpointProvider.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/DefaultCheckpointProvider.java @@ -21,14 +21,15 @@ import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpoint; -import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointStats; import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointingInfo; +import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointingInfo.TransformCheckpointingInfoBuilder; import org.elasticsearch.xpack.core.transform.transforms.TransformConfig; import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerPosition; import org.elasticsearch.xpack.core.transform.transforms.TransformProgress; import org.elasticsearch.xpack.transform.notifications.TransformAuditor; import org.elasticsearch.xpack.transform.persistence.TransformConfigManager; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -41,78 +42,6 @@ public class DefaultCheckpointProvider implements CheckpointProvider { // threshold when to audit concrete index names, above this threshold we only report the number of changes private static final int AUDIT_CONCRETED_SOURCE_INDEX_CHANGES = 10; - /** - * Builder for collecting checkpointing information for the purpose of _stats - */ - private static class TransformCheckpointingInfoBuilder { - private TransformIndexerPosition nextCheckpointPosition; - private TransformProgress nextCheckpointProgress; - private TransformCheckpoint lastCheckpoint; - private TransformCheckpoint nextCheckpoint; - private TransformCheckpoint sourceCheckpoint; - - TransformCheckpointingInfoBuilder() {} - - TransformCheckpointingInfo build() { - if (lastCheckpoint == null) { - lastCheckpoint = TransformCheckpoint.EMPTY; - } - if (nextCheckpoint == null) { - nextCheckpoint = TransformCheckpoint.EMPTY; - } - if (sourceCheckpoint == null) { - sourceCheckpoint = TransformCheckpoint.EMPTY; - } - - // checkpointstats requires a non-negative checkpoint number - long lastCheckpointNumber = lastCheckpoint.getCheckpoint() > 0 ? lastCheckpoint.getCheckpoint() : 0; - long nextCheckpointNumber = nextCheckpoint.getCheckpoint() > 0 ? nextCheckpoint.getCheckpoint() : 0; - - return new TransformCheckpointingInfo( - new TransformCheckpointStats( - lastCheckpointNumber, - null, - null, - lastCheckpoint.getTimestamp(), - lastCheckpoint.getTimeUpperBound() - ), - new TransformCheckpointStats( - nextCheckpointNumber, - nextCheckpointPosition, - nextCheckpointProgress, - nextCheckpoint.getTimestamp(), - nextCheckpoint.getTimeUpperBound() - ), - TransformCheckpoint.getBehind(lastCheckpoint, sourceCheckpoint) - ); - } - - public TransformCheckpointingInfoBuilder setLastCheckpoint(TransformCheckpoint lastCheckpoint) { - this.lastCheckpoint = lastCheckpoint; - return this; - } - - public TransformCheckpointingInfoBuilder setNextCheckpoint(TransformCheckpoint nextCheckpoint) { - this.nextCheckpoint = nextCheckpoint; - return this; - } - - public TransformCheckpointingInfoBuilder setSourceCheckpoint(TransformCheckpoint sourceCheckpoint) { - this.sourceCheckpoint = sourceCheckpoint; - return this; - } - - public TransformCheckpointingInfoBuilder setNextCheckpointProgress(TransformProgress nextCheckpointProgress) { - this.nextCheckpointProgress = nextCheckpointProgress; - return this; - } - - public TransformCheckpointingInfoBuilder setNextCheckpointPosition(TransformIndexerPosition nextCheckpointPosition) { - this.nextCheckpointPosition = nextCheckpointPosition; - return this; - } - } - private static final Logger logger = LogManager.getLogger(DefaultCheckpointProvider.class); protected final Client client; @@ -250,10 +179,10 @@ public void getCheckpointingInfo( TransformCheckpoint nextCheckpoint, TransformIndexerPosition nextCheckpointPosition, TransformProgress nextCheckpointProgress, - ActionListener listener + ActionListener listener ) { - - TransformCheckpointingInfoBuilder checkpointingInfoBuilder = new TransformCheckpointingInfoBuilder(); + TransformCheckpointingInfo.TransformCheckpointingInfoBuilder checkpointingInfoBuilder = + new TransformCheckpointingInfo.TransformCheckpointingInfoBuilder(); checkpointingInfoBuilder.setLastCheckpoint(lastCheckpoint) .setNextCheckpoint(nextCheckpoint) @@ -263,10 +192,10 @@ public void getCheckpointingInfo( long timestamp = System.currentTimeMillis(); getIndexCheckpoints(ActionListener.wrap(checkpointsByIndex -> { - checkpointingInfoBuilder.setSourceCheckpoint( - new TransformCheckpoint(transformConfig.getId(), timestamp, -1L, checkpointsByIndex, 0L) - ); - listener.onResponse(checkpointingInfoBuilder.build()); + TransformCheckpoint sourceCheckpoint = new TransformCheckpoint(transformConfig.getId(), timestamp, -1L, checkpointsByIndex, 0L); + checkpointingInfoBuilder.setSourceCheckpoint(sourceCheckpoint); + checkpointingInfoBuilder.setOperationsBehind(TransformCheckpoint.getBehind(lastCheckpoint, sourceCheckpoint)); + listener.onResponse(checkpointingInfoBuilder); }, listener::onFailure)); } @@ -275,21 +204,24 @@ public void getCheckpointingInfo( long lastCheckpointNumber, TransformIndexerPosition nextCheckpointPosition, TransformProgress nextCheckpointProgress, - ActionListener listener + ActionListener listener ) { - TransformCheckpointingInfoBuilder checkpointingInfoBuilder = new TransformCheckpointingInfoBuilder(); + TransformCheckpointingInfo.TransformCheckpointingInfoBuilder checkpointingInfoBuilder = + new TransformCheckpointingInfo.TransformCheckpointingInfoBuilder(); checkpointingInfoBuilder.setNextCheckpointPosition(nextCheckpointPosition).setNextCheckpointProgress(nextCheckpointProgress); - + checkpointingInfoBuilder.setLastCheckpoint(TransformCheckpoint.EMPTY); long timestamp = System.currentTimeMillis(); // <3> got the source checkpoint, notify the user ActionListener> checkpointsByIndexListener = ActionListener.wrap(checkpointsByIndex -> { - checkpointingInfoBuilder.setSourceCheckpoint( - new TransformCheckpoint(transformConfig.getId(), timestamp, -1L, checkpointsByIndex, 0L) + TransformCheckpoint sourceCheckpoint = new TransformCheckpoint(transformConfig.getId(), timestamp, -1L, checkpointsByIndex, 0L); + checkpointingInfoBuilder.setSourceCheckpoint(sourceCheckpoint); + checkpointingInfoBuilder.setOperationsBehind( + TransformCheckpoint.getBehind(checkpointingInfoBuilder.getLastCheckpoint(), sourceCheckpoint) ); - listener.onResponse(checkpointingInfoBuilder.build()); + listener.onResponse(checkpointingInfoBuilder); }, e -> { logger.debug( (Supplier) () -> new ParameterizedMessage( @@ -320,7 +252,8 @@ public void getCheckpointingInfo( // <1> got last checkpoint, get the next checkpoint ActionListener lastCheckpointListener = ActionListener.wrap(lastCheckpointObj -> { - checkpointingInfoBuilder.lastCheckpoint = lastCheckpointObj; + checkpointingInfoBuilder.setChangesLastDetectedAt(Instant.ofEpochMilli(lastCheckpointObj.getTimestamp())); + checkpointingInfoBuilder.setLastCheckpoint(lastCheckpointObj); transformConfigManager.getTransformCheckpoint(transformConfig.getId(), lastCheckpointNumber + 1, nextCheckpointListener); }, e -> { logger.debug( diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/TransformCheckpointService.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/TransformCheckpointService.java index b72ab2e2be814..64faf41462589 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/TransformCheckpointService.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/TransformCheckpointService.java @@ -11,7 +11,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.client.Client; import org.elasticsearch.xpack.core.transform.transforms.TimeSyncConfig; -import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointingInfo; +import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointingInfo.TransformCheckpointingInfoBuilder; import org.elasticsearch.xpack.core.transform.transforms.TransformConfig; import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerPosition; import org.elasticsearch.xpack.core.transform.transforms.TransformProgress; @@ -66,7 +66,7 @@ public void getCheckpointingInfo( final long lastCheckpointNumber, final TransformIndexerPosition nextCheckpointPosition, final TransformProgress nextCheckpointProgress, - final ActionListener listener + final ActionListener listener ) { // we need to retrieve the config first before we can defer the rest to the corresponding provider diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java index 48658e7b5ed24..d977d6b99f996 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java @@ -28,6 +28,7 @@ import org.elasticsearch.xpack.core.transform.TransformMessages; import org.elasticsearch.xpack.core.transform.action.StartTransformAction; import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointingInfo; +import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointingInfo.TransformCheckpointingInfoBuilder; import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerPosition; import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerStats; import org.elasticsearch.xpack.core.transform.transforms.TransformState; @@ -159,9 +160,22 @@ public void getCheckpointingInfo( TransformCheckpointService transformsCheckpointService, ActionListener listener ) { + ActionListener checkPointInfoListener = ActionListener.wrap(infoBuilder -> { + if (context.getChangesLastDetectedAt() != null) { + infoBuilder.setChangesLastDetectedAt(context.getChangesLastDetectedAt()); + } + listener.onResponse(infoBuilder.build()); + }, listener::onFailure); + ClientTransformIndexer indexer = getIndexer(); if (indexer == null) { - transformsCheckpointService.getCheckpointingInfo(transform.getId(), context.getCheckpoint(), initialPosition, null, listener); + transformsCheckpointService.getCheckpointingInfo( + transform.getId(), + context.getCheckpoint(), + initialPosition, + null, + checkPointInfoListener + ); return; } indexer.getCheckpointProvider() @@ -170,13 +184,7 @@ public void getCheckpointingInfo( indexer.getNextCheckpoint(), indexer.getPosition(), indexer.getProgress(), - ActionListener.wrap(info -> { - if (context.getChangesLastDetectedAt() == null) { - listener.onResponse(info); - } else { - listener.onResponse(info.setChangesLastDetectedAt(context.getChangesLastDetectedAt())); - } - }, listener::onFailure) + checkPointInfoListener ); } diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/checkpoint/TransformCheckpointServiceNodeTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/checkpoint/TransformCheckpointServiceNodeTests.java index 5d36b9b164df3..7af774f4a6faf 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/checkpoint/TransformCheckpointServiceNodeTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/checkpoint/TransformCheckpointServiceNodeTests.java @@ -39,12 +39,13 @@ import org.elasticsearch.index.warmer.WarmerStats; import org.elasticsearch.search.suggest.completion.CompletionStats; import org.elasticsearch.test.client.NoOpClient; -import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerPosition; -import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerPositionTests; import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpoint; import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointStats; import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointingInfo; +import org.elasticsearch.xpack.core.transform.transforms.TransformCheckpointingInfo.TransformCheckpointingInfoBuilder; import org.elasticsearch.xpack.core.transform.transforms.TransformConfigTests; +import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerPosition; +import org.elasticsearch.xpack.core.transform.transforms.TransformIndexerPositionTests; import org.elasticsearch.xpack.core.transform.transforms.TransformProgress; import org.elasticsearch.xpack.core.transform.transforms.TransformProgressTests; import org.elasticsearch.xpack.transform.TransformSingleNodeTestCase; @@ -54,6 +55,7 @@ import org.junit.Before; import java.nio.file.Path; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -73,7 +75,7 @@ public class TransformCheckpointServiceNodeTests extends TransformSingleNodeTest private static MockClientForCheckpointing mockClientForCheckpointing = null; private IndexBasedTransformConfigManager transformsConfigManager; - private TransformCheckpointService transformsCheckpointService; + private TransformCheckpointService transformCheckpointService; private class MockClientForCheckpointing extends NoOpClient { @@ -136,7 +138,7 @@ public void createComponents() { // use a mock for the checkpoint service TransformAuditor mockAuditor = mock(TransformAuditor.class); - transformsCheckpointService = new TransformCheckpointService(mockClientForCheckpointing, transformsConfigManager, mockAuditor); + transformCheckpointService = new TransformCheckpointService(mockClientForCheckpointing, transformsConfigManager, mockAuditor); } @AfterClass @@ -255,11 +257,12 @@ public void testGetCheckpointStats() throws InterruptedException { TransformCheckpointingInfo checkpointInfo = new TransformCheckpointingInfo( new TransformCheckpointStats(1, null, null, timestamp, 0L), new TransformCheckpointStats(2, position, progress, timestamp + 100L, 0L), - 30L + 30L, + Instant.ofEpochMilli(timestamp) ); assertAsync( - listener -> transformsCheckpointService.getCheckpointingInfo(transformId, 1, position, progress, listener), + listener -> getCheckpoint(transformCheckpointService, transformId, 1, position, progress, listener), checkpointInfo, null, null @@ -269,10 +272,11 @@ public void testGetCheckpointStats() throws InterruptedException { checkpointInfo = new TransformCheckpointingInfo( new TransformCheckpointStats(1, null, null, timestamp, 0L), new TransformCheckpointStats(2, position, progress, timestamp + 100L, 0L), - 63L + 63L, + Instant.ofEpochMilli(timestamp) ); assertAsync( - listener -> transformsCheckpointService.getCheckpointingInfo(transformId, 1, position, progress, listener), + listener -> getCheckpoint(transformCheckpointService, transformId, 1, position, progress, listener), checkpointInfo, null, null @@ -283,10 +287,11 @@ public void testGetCheckpointStats() throws InterruptedException { checkpointInfo = new TransformCheckpointingInfo( new TransformCheckpointStats(1, null, null, timestamp, 0L), new TransformCheckpointStats(2, position, progress, timestamp + 100L, 0L), - 0L + 0L, + Instant.ofEpochMilli(timestamp) ); assertAsync( - listener -> transformsCheckpointService.getCheckpointingInfo(transformId, 1, position, progress, listener), + listener -> getCheckpoint(transformCheckpointService, transformId, 1, position, progress, listener), checkpointInfo, null, null @@ -343,4 +348,25 @@ private static ShardStats[] createShardStats(Map checkpoints) { return shardStats.toArray(new ShardStats[0]); } + private static void getCheckpoint( + TransformCheckpointService transformCheckpointService, + String transformId, + long lastCheckpointNumber, + TransformIndexerPosition nextCheckpointPosition, + TransformProgress nextCheckpointProgress, + ActionListener listener + ) { + ActionListener checkPointInfoListener = ActionListener.wrap( + infoBuilder -> { listener.onResponse(infoBuilder.build()); }, + listener::onFailure + ); + transformCheckpointService.getCheckpointingInfo( + transformId, + lastCheckpointNumber, + nextCheckpointPosition, + nextCheckpointProgress, + checkPointInfoListener + ); + } + } From 27abe2e0ea2a1a350ed83fc30c8974d04439ace5 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Fri, 20 Dec 2019 10:22:23 +0100 Subject: [PATCH 294/686] Only auto-expand replicas with allocation filtering when all nodes upgraded (#50361) Follow-up to #48974 that ensures that replicas are only auto-expanded according to allocation filtering rules once all nodes are upgraded to a version that supports this. Helps with orchestrating cluster upgrades. --- .../elasticsearch/upgrades/RecoveryIT.java | 35 ++++++++++ .../cluster/metadata/AutoExpandReplicas.java | 14 ++-- .../metadata/AutoExpandReplicasTests.java | 64 ++++++++++++++++++- 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java index 7bd52d266914d..532b6d8c11b09 100644 --- a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java +++ b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java @@ -732,4 +732,39 @@ public void testTurnOffTranslogRetentionAfterUpgraded() throws Exception { assertEmptyTranslog(index); } } + + public void testAutoExpandIndicesDuringRollingUpgrade() throws Exception { + final String indexName = "test-auto-expand-filtering"; + final Version minimumNodeVersion = minimumNodeVersion(); + + Response response = client().performRequest(new Request("GET", "_nodes")); + ObjectPath objectPath = ObjectPath.createFromResponse(response); + final Map nodeMap = objectPath.evaluate("nodes"); + List nodes = new ArrayList<>(nodeMap.keySet()); + + if (CLUSTER_TYPE == ClusterType.OLD) { + createIndex(indexName, Settings.builder() + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, randomInt(2)) + .put(IndexMetaData.SETTING_AUTO_EXPAND_REPLICAS, "0-all") + .put(IndexMetaData.INDEX_ROUTING_EXCLUDE_GROUP_PREFIX + "._id", nodes.get(randomInt(2))) + .build()); + } + + ensureGreen(indexName); + + final int numberOfReplicas = Integer.parseInt( + getIndexSettingsAsMap(indexName).get(IndexMetaData.SETTING_NUMBER_OF_REPLICAS).toString()); + if (minimumNodeVersion.onOrAfter(Version.V_7_6_0)) { + assertEquals(nodes.size() - 2, numberOfReplicas); + } else { + assertEquals(nodes.size() - 1, numberOfReplicas); + } + } + + @SuppressWarnings("unchecked") + private Map getIndexSettingsAsMap(String index) throws IOException { + Map indexSettings = getIndexSettings(index); + return (Map)((Map) indexSettings.get(index)).get("settings"); + } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/AutoExpandReplicas.java b/server/src/main/java/org/elasticsearch/cluster/metadata/AutoExpandReplicas.java index 346d755c37916..ffd1c2a8263c6 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/AutoExpandReplicas.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/AutoExpandReplicas.java @@ -19,6 +19,7 @@ package org.elasticsearch.cluster.metadata; import com.carrotsearch.hppc.cursors.ObjectCursor; +import org.elasticsearch.Version; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.routing.allocation.RoutingAllocation; import org.elasticsearch.cluster.routing.allocation.decider.Decision; @@ -105,11 +106,16 @@ int getMaxReplicas(int numDataNodes) { private OptionalInt getDesiredNumberOfReplicas(IndexMetaData indexMetaData, RoutingAllocation allocation) { if (enabled) { int numMatchingDataNodes = 0; - for (ObjectCursor cursor : allocation.nodes().getDataNodes().values()) { - Decision decision = allocation.deciders().shouldAutoExpandToNode(indexMetaData, cursor.value, allocation); - if (decision.type() != Decision.Type.NO) { - numMatchingDataNodes ++; + // Only start using new logic once all nodes are migrated to 7.6.0, avoiding disruption during an upgrade + if (allocation.nodes().getMinNodeVersion().onOrAfter(Version.V_7_6_0)) { + for (ObjectCursor cursor : allocation.nodes().getDataNodes().values()) { + Decision decision = allocation.deciders().shouldAutoExpandToNode(indexMetaData, cursor.value, allocation); + if (decision.type() != Decision.Type.NO) { + numMatchingDataNodes ++; + } } + } else { + numMatchingDataNodes = allocation.nodes().getDataNodes().size(); } final int min = getMinReplicas(); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/AutoExpandReplicasTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/AutoExpandReplicasTests.java index f78104201c9fb..32d25b09fa3c0 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/AutoExpandReplicasTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/AutoExpandReplicasTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.Version; import org.elasticsearch.action.admin.cluster.reroute.ClusterRerouteRequest; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.replication.ClusterStateCreationUtils; import org.elasticsearch.cluster.ClusterState; @@ -32,6 +33,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.indices.cluster.ClusterStateChanges; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; @@ -46,6 +48,7 @@ import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_AUTO_EXPAND_REPLICAS; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.isIn; @@ -104,12 +107,15 @@ public void testInvalidValues() { private static final AtomicInteger nodeIdGenerator = new AtomicInteger(); - protected DiscoveryNode createNode(DiscoveryNodeRole... mustHaveRoles) { + protected DiscoveryNode createNode(Version version, DiscoveryNodeRole... mustHaveRoles) { Set roles = new HashSet<>(randomSubsetOf(DiscoveryNodeRole.BUILT_IN_ROLES)); Collections.addAll(roles, mustHaveRoles); final String id = String.format(Locale.ROOT, "node_%03d", nodeIdGenerator.incrementAndGet()); - return new DiscoveryNode(id, id, buildNewFakeTransportAddress(), Collections.emptyMap(), roles, - Version.CURRENT); + return new DiscoveryNode(id, id, buildNewFakeTransportAddress(), Collections.emptyMap(), roles, version); + } + + protected DiscoveryNode createNode(DiscoveryNodeRole... mustHaveRoles) { + return createNode(Version.CURRENT, mustHaveRoles); } /** @@ -200,4 +206,56 @@ public void testAutoExpandWhenNodeLeavesAndPossiblyRejoins() throws InterruptedE terminate(threadPool); } } + + public void testOnlyAutoExpandAllocationFilteringAfterAllNodesUpgraded() { + final ThreadPool threadPool = new TestThreadPool(getClass().getName()); + final ClusterStateChanges cluster = new ClusterStateChanges(xContentRegistry(), threadPool); + + try { + List allNodes = new ArrayList<>(); + DiscoveryNode oldNode = createNode(VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_5_1), + DiscoveryNodeRole.MASTER_ROLE, DiscoveryNodeRole.DATA_ROLE); // local node is the master + allNodes.add(oldNode); + ClusterState state = ClusterStateCreationUtils.state(oldNode, oldNode, allNodes.toArray(new DiscoveryNode[0])); + + CreateIndexRequest request = new CreateIndexRequest("index", + Settings.builder() + .put(SETTING_NUMBER_OF_SHARDS, 1) + .put(SETTING_AUTO_EXPAND_REPLICAS, "0-all").build()) + .waitForActiveShards(ActiveShardCount.NONE); + state = cluster.createIndex(state, request); + assertTrue(state.metaData().hasIndex("index")); + while (state.routingTable().index("index").shard(0).allShardsStarted() == false) { + logger.info(state); + state = cluster.applyStartedShards(state, + state.routingTable().index("index").shard(0).shardsWithState(ShardRoutingState.INITIALIZING)); + state = cluster.reroute(state, new ClusterRerouteRequest()); + } + + DiscoveryNode newNode = createNode(Version.V_7_6_0, + DiscoveryNodeRole.MASTER_ROLE, DiscoveryNodeRole.DATA_ROLE); // local node is the master + + state = cluster.addNodes(state, Collections.singletonList(newNode)); + + // use allocation filtering + state = cluster.updateSettings(state, new UpdateSettingsRequest("index").settings(Settings.builder() + .put(IndexMetaData.INDEX_ROUTING_EXCLUDE_GROUP_PREFIX + "._name", oldNode.getName()).build())); + + while (state.routingTable().index("index").shard(0).allShardsStarted() == false) { + logger.info(state); + state = cluster.applyStartedShards(state, + state.routingTable().index("index").shard(0).shardsWithState(ShardRoutingState.INITIALIZING)); + state = cluster.reroute(state, new ClusterRerouteRequest()); + } + + // check that presence of old node means that auto-expansion does not take allocation filtering into account + assertThat(state.routingTable().index("index").shard(0).size(), equalTo(2)); + + // remove old node and check that auto-expansion takes allocation filtering into account + state = cluster.removeNodes(state, Collections.singletonList(oldNode)); + assertThat(state.routingTable().index("index").shard(0).size(), equalTo(1)); + } finally { + terminate(threadPool); + } + } } From 8fcc633e16f4e0decab0cd3fb611ce19781a90ad Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Fri, 20 Dec 2019 10:25:39 +0100 Subject: [PATCH 295/686] Improve FutureUtils.get exception handling (#50339) FutureUtils.get() would unwrap ElasticsearchWrapperExceptions. This is trappy, since nearly all usages of FutureUtils.get() expected only to not have to deal with checked exceptions. In particular, StepListener builds upon ListenableFuture which uses FutureUtils.get to be informed about the exception passed to onFailure. This had the bad consequence of masking away any exception that was an ElasticsearchWrapperException like RemoteTransportException. Specifically for recovery, this made CircuitBreakerExceptions happening on the target node look like they originated from the source node. The only usage that expected that behaviour was AdapterActionFuture. The unwrap behaviour has been moved to that class. --- .../action/support/AdapterActionFuture.java | 21 ++++++++++++-- .../action/index/MappingUpdatedAction.java | 10 ++----- .../common/util/concurrent/FutureUtils.java | 14 +--------- .../action/StepListenerTests.java | 18 ++++++++++++ .../support/AdapterActionFutureTests.java | 28 +++++++++++++++++++ 5 files changed, 68 insertions(+), 23 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/support/AdapterActionFuture.java b/server/src/main/java/org/elasticsearch/action/support/AdapterActionFuture.java index 528750ba89b90..c37e89b8396b8 100644 --- a/server/src/main/java/org/elasticsearch/action/support/AdapterActionFuture.java +++ b/server/src/main/java/org/elasticsearch/action/support/AdapterActionFuture.java @@ -19,11 +19,13 @@ package org.elasticsearch.action.support; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionFuture; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.BaseFuture; import org.elasticsearch.common.util.concurrent.FutureUtils; +import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException; import java.util.concurrent.TimeUnit; @@ -31,7 +33,11 @@ public abstract class AdapterActionFuture extends BaseFuture implements @Override public T actionGet() { - return FutureUtils.get(this); + try { + return FutureUtils.get(this); + } catch (ElasticsearchException e) { + throw unwrapEsException(e); + } } @Override @@ -51,7 +57,11 @@ public T actionGet(TimeValue timeout) { @Override public T actionGet(long timeout, TimeUnit unit) { - return FutureUtils.get(this, timeout, unit); + try { + return FutureUtils.get(this, timeout, unit); + } catch (ElasticsearchException e) { + throw unwrapEsException(e); + } } @Override @@ -66,4 +76,11 @@ public void onFailure(Exception e) { protected abstract T convert(L listenerResponse); + private static RuntimeException unwrapEsException(ElasticsearchException esEx) { + Throwable root = esEx.unwrapCause(); + if (root instanceof RuntimeException) { + return (RuntimeException) root; + } + return new UncategorizedExecutionException("Failed execution", root); + } } diff --git a/server/src/main/java/org/elasticsearch/cluster/action/index/MappingUpdatedAction.java b/server/src/main/java/org/elasticsearch/cluster/action/index/MappingUpdatedAction.java index c25183ece82e9..874b5eb00646c 100644 --- a/server/src/main/java/org/elasticsearch/cluster/action/index/MappingUpdatedAction.java +++ b/server/src/main/java/org/elasticsearch/cluster/action/index/MappingUpdatedAction.java @@ -19,7 +19,6 @@ package org.elasticsearch.cluster.action.index; -import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.action.support.master.MasterNodeRequest; @@ -31,7 +30,6 @@ import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.util.concurrent.FutureUtils; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.Index; import org.elasticsearch.index.mapper.Mapping; @@ -72,7 +70,7 @@ public void setClient(Client client) { public void updateMappingOnMaster(Index index, Mapping mappingUpdate, ActionListener listener) { client.preparePutMapping().setConcreteIndex(index).setSource(mappingUpdate.toString(), XContentType.JSON) .setMasterNodeTimeout(dynamicMappingUpdateTimeout).setTimeout(TimeValue.ZERO) - .execute(new ActionListener() { + .execute(new ActionListener<>() { @Override public void onResponse(AcknowledgedResponse acknowledgedResponse) { listener.onResponse(null); @@ -80,12 +78,8 @@ public void onResponse(AcknowledgedResponse acknowledgedResponse) { @Override public void onFailure(Exception e) { - listener.onFailure(unwrapException(e)); + listener.onFailure(e); } }); } - - private static Exception unwrapException(Exception cause) { - return cause instanceof ElasticsearchException ? FutureUtils.unwrapEsException((ElasticsearchException) cause) : cause; - } } diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/FutureUtils.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/FutureUtils.java index 15e26779071ec..236dc9c716266 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/FutureUtils.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/FutureUtils.java @@ -19,7 +19,6 @@ package org.elasticsearch.common.util.concurrent; -import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.SuppressForbidden; @@ -86,21 +85,10 @@ public static T get(Future future, long timeout, TimeUnit unit) { } public static RuntimeException rethrowExecutionException(ExecutionException e) { - if (e.getCause() instanceof ElasticsearchException) { - ElasticsearchException esEx = (ElasticsearchException) e.getCause(); - return unwrapEsException(esEx); - } else if (e.getCause() instanceof RuntimeException) { + if (e.getCause() instanceof RuntimeException) { return (RuntimeException) e.getCause(); } else { return new UncategorizedExecutionException("Failed execution", e); } } - - public static RuntimeException unwrapEsException(ElasticsearchException esEx) { - Throwable root = esEx.unwrapCause(); - if (root instanceof ElasticsearchException || root instanceof RuntimeException) { - return (RuntimeException) root; - } - return new UncategorizedExecutionException("Failed execution", root); - } } diff --git a/server/src/test/java/org/elasticsearch/action/StepListenerTests.java b/server/src/test/java/org/elasticsearch/action/StepListenerTests.java index 15e88830e47e9..df57b30c52276 100644 --- a/server/src/test/java/org/elasticsearch/action/StepListenerTests.java +++ b/server/src/test/java/org/elasticsearch/action/StepListenerTests.java @@ -22,11 +22,13 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteTransportException; import org.junit.After; import org.junit.Before; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import static org.hamcrest.Matchers.equalTo; @@ -110,4 +112,20 @@ private void executeAction(Runnable runnable) { runnable.run(); } } + + /** + * This test checks that we no longer unwrap exceptions when using StepListener. + */ + public void testNoUnwrap() { + StepListener step = new StepListener<>(); + step.onFailure(new RemoteTransportException("test", new RuntimeException("expected"))); + AtomicReference exception = new AtomicReference<>(); + step.whenComplete(null, e -> { + exception.set((RuntimeException) e); + }); + + assertEquals(RemoteTransportException.class, exception.get().getClass()); + RuntimeException e = expectThrows(RuntimeException.class, () -> step.result()); + assertEquals(RemoteTransportException.class, e.getClass()); + } } diff --git a/server/src/test/java/org/elasticsearch/action/support/AdapterActionFutureTests.java b/server/src/test/java/org/elasticsearch/action/support/AdapterActionFutureTests.java index a7405ddae8cce..b2c6a8c5ba2aa 100644 --- a/server/src/test/java/org/elasticsearch/action/support/AdapterActionFutureTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/AdapterActionFutureTests.java @@ -19,12 +19,16 @@ package org.elasticsearch.action.support; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.RemoteTransportException; import java.util.Objects; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -90,4 +94,28 @@ protected String convert(final Integer listenerResponse) { thread.join(); } + public void testUnwrapException() { + checkUnwrap(new RemoteTransportException("test", new RuntimeException()), RuntimeException.class, RemoteTransportException.class); + checkUnwrap(new RemoteTransportException("test", new Exception()), + UncategorizedExecutionException.class, RemoteTransportException.class); + checkUnwrap(new Exception(), UncategorizedExecutionException.class, Exception.class); + checkUnwrap(new ElasticsearchException("test", new Exception()), ElasticsearchException.class, ElasticsearchException.class); + } + + private void checkUnwrap(Exception exception, Class actionGetException, + Class getException) { + final AdapterActionFuture adapter = new AdapterActionFuture() { + @Override + protected Void convert(Void listenerResponse) { + fail(); + return null; + } + }; + + adapter.onFailure(exception); + assertEquals(actionGetException, expectThrows(RuntimeException.class, adapter::actionGet).getClass()); + assertEquals(actionGetException, expectThrows(RuntimeException.class, () -> adapter.actionGet(10, TimeUnit.SECONDS)).getClass()); + assertEquals(getException, expectThrows(ExecutionException.class, () -> adapter.get()).getCause().getClass()); + assertEquals(getException, expectThrows(ExecutionException.class, () -> adapter.get(10, TimeUnit.SECONDS)).getCause().getClass()); + } } From e8dfca0e80b50aa9816a2e573e14f7acd5964354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Fri, 20 Dec 2019 11:24:50 +0100 Subject: [PATCH 296/686] Get rid of maxClassesCardinality internal parameter (#50418) --- .../evaluation/classification/Precision.java | 19 ++++----------- .../evaluation/classification/Recall.java | 19 ++++----------- .../ClassificationEvaluationIT.java | 24 ++++++++++++++++--- 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java index f6bacbec05e7b..dd04f23710118 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; -import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; @@ -75,25 +74,15 @@ public static Precision fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } - private static final int DEFAULT_MAX_CLASSES_CARDINALITY = 1000; + private static final int MAX_CLASSES_CARDINALITY = 1000; - private final int maxClassesCardinality; private String actualField; private List topActualClassNames; private EvaluationMetricResult result; - public Precision() { - this((Integer) null); - } - - // Visible for testing - public Precision(@Nullable Integer maxClassesCardinality) { - this.maxClassesCardinality = maxClassesCardinality != null ? maxClassesCardinality : DEFAULT_MAX_CLASSES_CARDINALITY; - } + public Precision() {} - public Precision(StreamInput in) throws IOException { - this.maxClassesCardinality = DEFAULT_MAX_CLASSES_CARDINALITY; - } + public Precision(StreamInput in) throws IOException {} @Override public String getWriteableName() { @@ -115,7 +104,7 @@ public final Tuple, List> a AggregationBuilders.terms(ACTUAL_CLASSES_NAMES_AGG_NAME) .field(actualField) .order(List.of(BucketOrder.count(false), BucketOrder.key(true))) - .size(maxClassesCardinality)), + .size(MAX_CLASSES_CARDINALITY)), List.of()); } if (result == null) { // This is step 2 diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java index 522810e57e2dd..01bdbe6db230b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; -import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; @@ -69,24 +68,14 @@ public static Recall fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } - private static final int DEFAULT_MAX_CLASSES_CARDINALITY = 1000; + private static final int MAX_CLASSES_CARDINALITY = 1000; - private final int maxClassesCardinality; private String actualField; private EvaluationMetricResult result; - public Recall() { - this((Integer) null); - } - - // Visible for testing - public Recall(@Nullable Integer maxClassesCardinality) { - this.maxClassesCardinality = maxClassesCardinality != null ? maxClassesCardinality : DEFAULT_MAX_CLASSES_CARDINALITY; - } + public Recall() {} - public Recall(StreamInput in) throws IOException { - this.maxClassesCardinality = DEFAULT_MAX_CLASSES_CARDINALITY; - } + public Recall(StreamInput in) throws IOException {} @Override public String getWriteableName() { @@ -110,7 +99,7 @@ public final Tuple, List> a List.of( AggregationBuilders.terms(BY_ACTUAL_CLASS_AGG_NAME) .field(actualField) - .size(maxClassesCardinality) + .size(MAX_CLASSES_CARDINALITY) .subAggregation(AggregationBuilders.avg(PER_ACTUAL_CLASS_RECALL_AGG_NAME).script(script))), List.of( PipelineAggregatorBuilders.avgBucket( diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java index 14c2c3c9aca61..437b2ddbf5180 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java @@ -38,6 +38,7 @@ public class ClassificationEvaluationIT extends MlNativeDataFrameAnalyticsIntegT @Before public void setup() { + createAnimalsIndex(ANIMALS_DATA_INDEX); indexAnimalsData(ANIMALS_DATA_INDEX); } @@ -141,11 +142,12 @@ public void testEvaluate_Precision() { } public void testEvaluate_Precision_CardinalityTooHigh() { + indexDistinctAnimals(ANIMALS_DATA_INDEX, 1001); ElasticsearchStatusException e = expectThrows( ElasticsearchStatusException.class, () -> evaluateDataFrame( - ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new Precision(4))))); + ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new Precision())))); assertThat(e.getMessage(), containsString("Cardinality of field [animal_name] is too high")); } @@ -172,11 +174,12 @@ public void testEvaluate_Recall() { } public void testEvaluate_Recall_CardinalityTooHigh() { + indexDistinctAnimals(ANIMALS_DATA_INDEX, 1001); ElasticsearchStatusException e = expectThrows( ElasticsearchStatusException.class, () -> evaluateDataFrame( - ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new Recall(4))))); + ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new Recall())))); assertThat(e.getMessage(), containsString("Cardinality of field [animal_name] is too high")); } @@ -281,7 +284,7 @@ public void testEvaluate_ConfusionMatrixMetricWithUserProvidedSize() { assertThat(confusionMatrixResult.getOtherActualClassCount(), equalTo(2L)); } - private static void indexAnimalsData(String indexName) { + private static void createAnimalsIndex(String indexName) { client().admin().indices().prepareCreate(indexName) .addMapping("_doc", ANIMAL_NAME_FIELD, "type=keyword", @@ -291,7 +294,9 @@ private static void indexAnimalsData(String indexName) { IS_PREDATOR_FIELD, "type=boolean", IS_PREDATOR_PREDICTION_FIELD, "type=boolean") .get(); + } + private static void indexAnimalsData(String indexName) { List animalNames = List.of("dog", "cat", "mouse", "ant", "fox"); BulkRequestBuilder bulkRequestBuilder = client().prepareBulk() .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); @@ -315,4 +320,17 @@ private static void indexAnimalsData(String indexName) { fail("Failed to index data: " + bulkResponse.buildFailureMessage()); } } + + private static void indexDistinctAnimals(String indexName, int distinctAnimalCount) { + BulkRequestBuilder bulkRequestBuilder = client().prepareBulk() + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + for (int i = 0; i < distinctAnimalCount; i++) { + bulkRequestBuilder.add( + new IndexRequest(indexName).source(ANIMAL_NAME_FIELD, "animal_" + i, ANIMAL_NAME_PREDICTION_FIELD, randomAlphaOfLength(5))); + } + BulkResponse bulkResponse = bulkRequestBuilder.get(); + if (bulkResponse.hasFailures()) { + fail("Failed to index data: " + bulkResponse.buildFailureMessage()); + } + } } From 77ecdd8d2e76322cd8d4fddbe03ec885bbc7cfe5 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Fri, 20 Dec 2019 11:48:09 +0100 Subject: [PATCH 297/686] Fix testAutoExpandIndicesDuringRollingUpgrade (#50361) Follow-up to #50361 that fixes the test that does not work against older ES versions --- .../src/test/java/org/elasticsearch/upgrades/RecoveryIT.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java index 532b6d8c11b09..429687853e897 100644 --- a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java +++ b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java @@ -747,8 +747,10 @@ public void testAutoExpandIndicesDuringRollingUpgrade() throws Exception { .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, randomInt(2)) .put(IndexMetaData.SETTING_AUTO_EXPAND_REPLICAS, "0-all") - .put(IndexMetaData.INDEX_ROUTING_EXCLUDE_GROUP_PREFIX + "._id", nodes.get(randomInt(2))) .build()); + ensureGreen(indexName); + updateIndexSettings(indexName, + Settings.builder().put(IndexMetaData.INDEX_ROUTING_EXCLUDE_GROUP_PREFIX + "._id", nodes.get(randomInt(2)))); } ensureGreen(indexName); From d7be9aeff792b994c86c60eecd05bfc911577925 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Fri, 20 Dec 2019 11:59:09 +0100 Subject: [PATCH 298/686] Mute RecoverySourceHandlerTests.testCancelRecoveryDuringPhase1 Relates #50424 --- .../indices/recovery/RecoverySourceHandlerTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java index db9f52f942e63..a10a8fc0410b6 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java @@ -628,6 +628,7 @@ public void writeFileChunk(StoreFileMetaData md, long position, BytesReference c store.close(); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50424") public void testCancelRecoveryDuringPhase1() throws Exception { Store store = newStore(createTempDir("source"), false); IndexShard shard = mock(IndexShard.class); From a16d7c00555b65615b97a9a0b033d4f9d4f41380 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Fri, 20 Dec 2019 13:10:06 +0200 Subject: [PATCH 299/686] Fix NPE when `./gradlew run` without `--data-dir` (#50421) --- .../java/org/elasticsearch/gradle/testclusters/RunTask.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java index 07e5a6ae221f1..cedeff6a9cf5d 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java @@ -3,6 +3,7 @@ import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; @@ -49,7 +50,9 @@ public void setDataDir(String dataDirStr) { } @Input + @Optional public String getDataDir() { + if (dataDir == null) { return null;} return dataDir.toString(); } From 9d7fbe080404b47ff79a9c27685ff5e9682f5942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Fri, 20 Dec 2019 12:14:41 +0100 Subject: [PATCH 300/686] Make each analysis report desired field mappings to be copied (#50219) --- .../ml/dataframe/analyses/Classification.java | 8 + .../dataframe/analyses/DataFrameAnalysis.java | 11 + .../dataframe/analyses/OutlierDetection.java | 5 + .../ml/dataframe/analyses/Regression.java | 5 + .../analyses/ClassificationTests.java | 14 +- .../analyses/OutlierDetectionTests.java | 16 +- .../dataframe/analyses/RegressionTests.java | 14 +- .../ml/integration/ClassificationIT.java | 129 ++++++----- ...nsportExplainDataFrameAnalyticsAction.java | 12 +- ...ransportStartDataFrameAnalyticsAction.java | 3 +- .../ml/dataframe/DataFrameAnalyticsIndex.java | 81 +++++-- .../dataframe/DataFrameAnalyticsManager.java | 10 +- .../extractor/DataFrameDataExtractor.java | 5 +- .../DataFrameDataExtractorFactory.java | 8 +- .../extractor/ExtractedFieldsDetector.java | 31 +-- .../ExtractedFieldsDetectorFactory.java | 18 +- .../DataFrameAnalyticsIndexTests.java | 208 +++++++++++++++--- .../ExtractedFieldsDetectorTests.java | 111 ++++------ 18 files changed, 444 insertions(+), 245 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java index f3afd387e10b5..0e68d13895e25 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Classification.java @@ -246,6 +246,14 @@ public Map getFieldCardinalityLimits() { return Collections.singletonMap(dependentVariable, 2L); } + @Override + public Map getExplicitlyMappedFields(String resultsFieldName) { + return new HashMap<>() {{ + put(resultsFieldName + "." + predictionFieldName, dependentVariable); + put(resultsFieldName + ".top_classes.class_name", dependentVariable); + }}; + } + @Override public boolean supportsMissingValues() { return true; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/DataFrameAnalysis.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/DataFrameAnalysis.java index d0af0a452a474..74cdc5824cbdc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/DataFrameAnalysis.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/DataFrameAnalysis.java @@ -41,6 +41,17 @@ public interface DataFrameAnalysis extends ToXContentObject, NamedWriteable { */ Map getFieldCardinalityLimits(); + /** + * Returns fields for which the mappings should be copied from source index to destination index. + * Each entry of the returned {@link Map} is of the form: + * key - field path in the destination index + * value - field path in the source index from which the mapping should be taken + * + * @param resultsFieldName name of the results field under which all the results are stored + * @return {@link Map} containing fields for which the mappings should be copied from source index to destination index + */ + Map getExplicitlyMappedFields(String resultsFieldName); + /** * @return {@code true} if this analysis supports data frame rows with missing values */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetection.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetection.java index 70b3cfb9fe246..81c4673809368 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetection.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetection.java @@ -229,6 +229,11 @@ public Map getFieldCardinalityLimits() { return Collections.emptyMap(); } + @Override + public Map getExplicitlyMappedFields(String resultsFieldName) { + return Collections.emptyMap(); + } + @Override public boolean supportsMissingValues() { return false; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java index 27c8a3f2eb7ca..fe2927591312a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/Regression.java @@ -186,6 +186,11 @@ public Map getFieldCardinalityLimits() { return Collections.emptyMap(); } + @Override + public Map getExplicitlyMappedFields(String resultsFieldName) { + return Collections.singletonMap(resultsFieldName + "." + predictionFieldName, dependentVariable); + } + @Override public boolean supportsMissingValues() { return true; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java index 533839e40b524..1b988379fc218 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java @@ -23,7 +23,9 @@ import java.util.Map; import java.util.Set; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -162,8 +164,16 @@ public void testGetParams() { "prediction_field_type", "string"))); } - public void testFieldCardinalityLimitsIsNonNull() { - assertThat(createTestInstance().getFieldCardinalityLimits(), is(not(nullValue()))); + public void testRequiredFieldsIsNonEmpty() { + assertThat(createTestInstance().getRequiredFields(), is(not(empty()))); + } + + public void testFieldCardinalityLimitsIsNonEmpty() { + assertThat(createTestInstance().getFieldCardinalityLimits(), is(not(anEmptyMap()))); + } + + public void testFieldMappingsToCopyIsNonEmpty() { + assertThat(createTestInstance().getExplicitlyMappedFields(""), is(not(anEmptyMap()))); } public void testToXContent_GivenVersionBeforeRandomizeSeedWasIntroduced() throws IOException { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetectionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetectionTests.java index c35b9a3bad1af..5b7a23b46ff24 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetectionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/OutlierDetectionTests.java @@ -12,11 +12,11 @@ import java.io.IOException; import java.util.Map; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; public class OutlierDetectionTests extends AbstractSerializingTestCase { @@ -84,8 +84,16 @@ public void testGetParams_GivenExplicitValues() { assertThat(params.get(OutlierDetection.STANDARDIZATION_ENABLED.getPreferredName()), is(false)); } - public void testFieldCardinalityLimitsIsNonNull() { - assertThat(createTestInstance().getFieldCardinalityLimits(), is(not(nullValue()))); + public void testRequiredFieldsIsEmpty() { + assertThat(createTestInstance().getRequiredFields(), is(empty())); + } + + public void testFieldCardinalityLimitsIsEmpty() { + assertThat(createTestInstance().getFieldCardinalityLimits(), is(anEmptyMap())); + } + + public void testFieldMappingsToCopyIsEmpty() { + assertThat(createTestInstance().getExplicitlyMappedFields(""), is(anEmptyMap())); } public void testGetStateDocId() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java index 1a1dc79ad2a12..c7f89cc0413b5 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/RegressionTests.java @@ -19,7 +19,9 @@ import java.util.Collections; import java.util.Map; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -99,8 +101,16 @@ public void testGetParams() { equalTo(Map.of("dependent_variable", "foo", "prediction_field_name", "foo_prediction"))); } - public void testFieldCardinalityLimitsIsNonNull() { - assertThat(createTestInstance().getFieldCardinalityLimits(), is(not(nullValue()))); + public void testRequiredFieldsIsNonEmpty() { + assertThat(createTestInstance().getRequiredFields(), is(not(empty()))); + } + + public void testFieldCardinalityLimitsIsEmpty() { + assertThat(createTestInstance().getFieldCardinalityLimits(), is(anEmptyMap())); + } + + public void testFieldMappingsToCopyIsNonEmpty() { + assertThat(createTestInstance().getExplicitlyMappedFields(""), is(not(anEmptyMap()))); } public void testGetStateDocId() { diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java index 87fa5c30b0755..fc2739c0a70e3 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java @@ -7,6 +7,8 @@ import com.google.common.collect.Ordering; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.admin.indices.get.GetIndexAction; +import org.elasticsearch.action.admin.indices.get.GetIndexRequest; import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkResponse; @@ -39,6 +41,7 @@ import java.util.Set; import static java.util.stream.Collectors.toList; +import static org.elasticsearch.common.xcontent.support.XContentMapValues.extractValue; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -64,6 +67,7 @@ public class ClassificationIT extends MlNativeDataFrameAnalyticsIntegTestCase { private String jobId; private String sourceIndex; private String destIndex; + private boolean analysisUsesExistingDestIndex; @After public void cleanup() { @@ -72,6 +76,7 @@ public void cleanup() { public void testSingleNumericFeatureAndMixedTrainingAndNonTrainingRows() throws Exception { initialize("classification_single_numeric_feature_and_mixed_data_set"); + String predictedClassField = KEYWORD_FIELD + "_prediction"; indexData(sourceIndex, 300, 50, KEYWORD_FIELD); DataFrameAnalyticsConfig config = buildAnalytics(jobId, sourceIndex, destIndex, null, new Classification(KEYWORD_FIELD)); @@ -88,12 +93,9 @@ public void testSingleNumericFeatureAndMixedTrainingAndNonTrainingRows() throws SearchResponse sourceData = client().prepareSearch(sourceIndex).setTrackTotalHits(true).setSize(1000).get(); for (SearchHit hit : sourceData.getHits()) { Map destDoc = getDestDoc(config, hit); - Map resultsObject = getMlResultsObjectFromDestDoc(destDoc); - - assertThat(resultsObject.containsKey("keyword-field_prediction"), is(true)); - assertThat((String) resultsObject.get("keyword-field_prediction"), is(in(KEYWORD_FIELD_VALUES))); - assertThat(resultsObject.containsKey("is_training"), is(true)); - assertThat(resultsObject.get("is_training"), is(destDoc.containsKey(KEYWORD_FIELD))); + Map resultsObject = getFieldValue(destDoc, "ml"); + assertThat(getFieldValue(resultsObject, predictedClassField), is(in(KEYWORD_FIELD_VALUES))); + assertThat(getFieldValue(resultsObject, "is_training"), is(destDoc.containsKey(KEYWORD_FIELD))); assertTopClasses(resultsObject, 2, KEYWORD_FIELD, KEYWORD_FIELD_VALUES); } @@ -101,19 +103,21 @@ public void testSingleNumericFeatureAndMixedTrainingAndNonTrainingRows() throws assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); assertModelStatePersisted(stateDocId()); assertInferenceModelPersisted(jobId); + assertMlResultsFieldMappings(predictedClassField, "keyword"); assertThatAuditMessagesMatch(jobId, "Created analytics with analysis type [classification]", "Estimated memory usage for this analytics to be", "Starting analytics on node", "Started analytics", - "Creating destination index [" + destIndex + "]", + expectedDestIndexAuditMessage(), "Finished reindexing to destination index [" + destIndex + "]", "Finished analysis"); - assertEvaluation(KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "ml.keyword-field_prediction.keyword"); + assertEvaluation(KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "ml." + predictedClassField); } public void testWithOnlyTrainingRowsAndTrainingPercentIsHundred() throws Exception { initialize("classification_only_training_data_and_training_percent_is_100"); + String predictedClassField = KEYWORD_FIELD + "_prediction"; indexData(sourceIndex, 300, 0, KEYWORD_FIELD); DataFrameAnalyticsConfig config = buildAnalytics(jobId, sourceIndex, destIndex, null, new Classification(KEYWORD_FIELD)); @@ -129,12 +133,10 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsHundred() throws Excepti client().admin().indices().refresh(new RefreshRequest(destIndex)); SearchResponse sourceData = client().prepareSearch(sourceIndex).setTrackTotalHits(true).setSize(1000).get(); for (SearchHit hit : sourceData.getHits()) { - Map resultsObject = getMlResultsObjectFromDestDoc(getDestDoc(config, hit)); - - assertThat(resultsObject.containsKey("keyword-field_prediction"), is(true)); - assertThat((String) resultsObject.get("keyword-field_prediction"), is(in(KEYWORD_FIELD_VALUES))); - assertThat(resultsObject.containsKey("is_training"), is(true)); - assertThat(resultsObject.get("is_training"), is(true)); + Map destDoc = getDestDoc(config, hit); + Map resultsObject = getFieldValue(destDoc, "ml"); + assertThat(getFieldValue(resultsObject, predictedClassField), is(in(KEYWORD_FIELD_VALUES))); + assertThat(getFieldValue(resultsObject, "is_training"), is(true)); assertTopClasses(resultsObject, 2, KEYWORD_FIELD, KEYWORD_FIELD_VALUES); } @@ -142,19 +144,22 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsHundred() throws Excepti assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); assertModelStatePersisted(stateDocId()); assertInferenceModelPersisted(jobId); + assertMlResultsFieldMappings(predictedClassField, "keyword"); assertThatAuditMessagesMatch(jobId, "Created analytics with analysis type [classification]", "Estimated memory usage for this analytics to be", "Starting analytics on node", "Started analytics", - "Creating destination index [" + destIndex + "]", + expectedDestIndexAuditMessage(), "Finished reindexing to destination index [" + destIndex + "]", "Finished analysis"); - assertEvaluation(KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "ml.keyword-field_prediction.keyword"); + assertEvaluation(KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "ml." + predictedClassField); } - public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty( - String jobId, String dependentVariable, List dependentVariableValues) throws Exception { + public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty(String jobId, + String dependentVariable, + List dependentVariableValues, + String expectedMappingTypeForPredictedField) throws Exception { initialize(jobId); String predictedClassField = dependentVariable + "_prediction"; indexData(sourceIndex, 300, 0, dependentVariable); @@ -181,16 +186,13 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty( client().admin().indices().refresh(new RefreshRequest(destIndex)); SearchResponse sourceData = client().prepareSearch(sourceIndex).setTrackTotalHits(true).setSize(1000).get(); for (SearchHit hit : sourceData.getHits()) { - Map resultsObject = getMlResultsObjectFromDestDoc(getDestDoc(config, hit)); - assertThat(resultsObject.containsKey(predictedClassField), is(true)); - @SuppressWarnings("unchecked") - T predictedClassValue = (T) resultsObject.get(predictedClassField); - assertThat(predictedClassValue, is(in(dependentVariableValues))); + Map destDoc = getDestDoc(config, hit); + Map resultsObject = getFieldValue(destDoc, "ml"); + assertThat(getFieldValue(resultsObject, predictedClassField), is(in(dependentVariableValues))); assertTopClasses(resultsObject, numTopClasses, dependentVariable, dependentVariableValues); - assertThat(resultsObject.containsKey("is_training"), is(true)); // Let's just assert there's both training and non-training results - if ((boolean) resultsObject.get("is_training")) { + if (getFieldValue(resultsObject, "is_training")) { trainingRowsCount++; } else { nonTrainingRowsCount++; @@ -203,40 +205,39 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty( assertThat(searchStoredProgress(jobId).getHits().getTotalHits().value, equalTo(1L)); assertModelStatePersisted(stateDocId()); assertInferenceModelPersisted(jobId); + assertMlResultsFieldMappings(predictedClassField, expectedMappingTypeForPredictedField); assertThatAuditMessagesMatch(jobId, "Created analytics with analysis type [classification]", "Estimated memory usage for this analytics to be", "Starting analytics on node", "Started analytics", - "Creating destination index [" + destIndex + "]", + expectedDestIndexAuditMessage(), "Finished reindexing to destination index [" + destIndex + "]", "Finished analysis"); + assertEvaluation(dependentVariable, dependentVariableValues, "ml." + predictedClassField); } public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty_DependentVariableIsKeyword() throws Exception { testWithOnlyTrainingRowsAndTrainingPercentIsFifty( - "classification_training_percent_is_50_keyword", KEYWORD_FIELD, KEYWORD_FIELD_VALUES); - assertEvaluation(KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "ml.keyword-field_prediction.keyword"); + "classification_training_percent_is_50_keyword", KEYWORD_FIELD, KEYWORD_FIELD_VALUES, "keyword"); } public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty_DependentVariableIsInteger() throws Exception { testWithOnlyTrainingRowsAndTrainingPercentIsFifty( - "classification_training_percent_is_50_integer", DISCRETE_NUMERICAL_FIELD, DISCRETE_NUMERICAL_FIELD_VALUES); - assertEvaluation(DISCRETE_NUMERICAL_FIELD, DISCRETE_NUMERICAL_FIELD_VALUES, "ml.discrete-numerical-field_prediction"); + "classification_training_percent_is_50_integer", DISCRETE_NUMERICAL_FIELD, DISCRETE_NUMERICAL_FIELD_VALUES, "integer"); } public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty_DependentVariableIsDouble() throws Exception { ElasticsearchStatusException e = expectThrows( ElasticsearchStatusException.class, () -> testWithOnlyTrainingRowsAndTrainingPercentIsFifty( - "classification_training_percent_is_50_double", NUMERICAL_FIELD, NUMERICAL_FIELD_VALUES)); + "classification_training_percent_is_50_double", NUMERICAL_FIELD, NUMERICAL_FIELD_VALUES, null)); assertThat(e.getMessage(), startsWith("invalid types [double] for required field [numerical-field];")); } public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty_DependentVariableIsBoolean() throws Exception { testWithOnlyTrainingRowsAndTrainingPercentIsFifty( - "classification_training_percent_is_50_boolean", BOOLEAN_FIELD, BOOLEAN_FIELD_VALUES); - assertEvaluation(BOOLEAN_FIELD, BOOLEAN_FIELD_VALUES, "ml.boolean-field_prediction"); + "classification_training_percent_is_50_boolean", BOOLEAN_FIELD, BOOLEAN_FIELD_VALUES, "boolean"); } public void testDependentVariableCardinalityTooHighError() throws Exception { @@ -282,6 +283,7 @@ public void testTwoJobsWithSameRandomizeSeedUseSameTrainingSet() throws Exceptio String sourceIndex = "classification_two_jobs_with_same_randomize_seed_source"; String dependentVariable = KEYWORD_FIELD; + createIndex(sourceIndex); // We use 100 rows as we can't set this too low. If too low it is possible // we only train with rows of one of the two classes which leads to a failure. indexData(sourceIndex, 100, 0, dependentVariable); @@ -355,17 +357,24 @@ private void initialize(String jobId) { this.jobId = jobId; this.sourceIndex = jobId + "_source_index"; this.destIndex = sourceIndex + "_results"; + this.analysisUsesExistingDestIndex = randomBoolean(); + createIndex(sourceIndex); + if (analysisUsesExistingDestIndex) { + createIndex(destIndex); + } } - private static void indexData(String sourceIndex, int numTrainingRows, int numNonTrainingRows, String dependentVariable) { - client().admin().indices().prepareCreate(sourceIndex) + private static void createIndex(String index) { + client().admin().indices().prepareCreate(index) .addMapping("_doc", BOOLEAN_FIELD, "type=boolean", NUMERICAL_FIELD, "type=double", DISCRETE_NUMERICAL_FIELD, "type=integer", KEYWORD_FIELD, "type=keyword") .get(); + } + private static void indexData(String sourceIndex, int numTrainingRows, int numNonTrainingRows, String dependentVariable) { BulkRequestBuilder bulkRequestBuilder = client().prepareBulk() .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); for (int i = 0; i < numTrainingRows; i++) { @@ -407,34 +416,30 @@ private static Map getDestDoc(DataFrameAnalyticsConfig config, S Map sourceDoc = hit.getSourceAsMap(); Map destDoc = destDocGetResponse.getSource(); for (String field : sourceDoc.keySet()) { - assertThat(destDoc.containsKey(field), is(true)); + assertThat(destDoc, hasKey(field)); assertThat(destDoc.get(field), equalTo(sourceDoc.get(field))); } return destDoc; } - private static Map getMlResultsObjectFromDestDoc(Map destDoc) { - assertThat(destDoc.containsKey("ml"), is(true)); - @SuppressWarnings("unchecked") - Map resultsObject = (Map) destDoc.get("ml"); - return resultsObject; + /** + * Wrapper around extractValue with implicit casting to the appropriate type. + */ + private static T getFieldValue(Map doc, String... path) { + return (T)extractValue(doc, path); } - @SuppressWarnings("unchecked") - private static void assertTopClasses( - Map resultsObject, - int numTopClasses, - String dependentVariable, - List dependentVariableValues) { - assertThat(resultsObject.containsKey("top_classes"), is(true)); - List> topClasses = (List>) resultsObject.get("top_classes"); + private static void assertTopClasses(Map resultsObject, + int numTopClasses, + String dependentVariable, + List dependentVariableValues) { + List> topClasses = getFieldValue(resultsObject, "top_classes"); assertThat(topClasses, hasSize(numTopClasses)); List classNames = new ArrayList<>(topClasses.size()); List classProbabilities = new ArrayList<>(topClasses.size()); for (Map topClass : topClasses) { - assertThat(topClass, allOf(hasKey("class_name"), hasKey("class_probability"))); - classNames.add((T) topClass.get("class_name")); - classProbabilities.add((Double) topClass.get("class_probability")); + classNames.add(getFieldValue(topClass, "class_name")); + classProbabilities.add(getFieldValue(topClass, "class_probability")); } // Assert that all the predicted class names come from the set of dependent variable values. classNames.forEach(className -> assertThat(className, is(in(dependentVariableValues)))); @@ -507,7 +512,25 @@ private void assertEvaluation(String dependentVariable, List dependentVar } } - protected String stateDocId() { + private void assertMlResultsFieldMappings(String predictedClassField, String expectedType) { + Map mappings = + client() + .execute(GetIndexAction.INSTANCE, new GetIndexRequest().indices(destIndex)) + .actionGet() + .mappings() + .get(destIndex) + .sourceAsMap(); + assertThat(getFieldValue(mappings, "properties", "ml", "properties", predictedClassField, "type"), equalTo(expectedType)); + assertThat( + getFieldValue(mappings, "properties", "ml", "properties", "top_classes", "properties", "class_name", "type"), + equalTo(expectedType)); + } + + private String stateDocId() { return jobId + "_classification_state#1"; } + + private String expectedDestIndexAuditMessage() { + return (analysisUsesExistingDestIndex ? "Using existing" : "Creating") + " destination index [" + destIndex + "]"; + } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportExplainDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportExplainDataFrameAnalyticsAction.java index 7f19deb8d5ba0..46393a3277153 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportExplainDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportExplainDataFrameAnalyticsAction.java @@ -84,12 +84,12 @@ protected void doExecute(Task task, private void explain(Task task, PutDataFrameAnalyticsAction.Request request, ActionListener listener) { ExtractedFieldsDetectorFactory extractedFieldsDetectorFactory = new ExtractedFieldsDetectorFactory(client); - extractedFieldsDetectorFactory.createFromSource(request.getConfig(), true, ActionListener.wrap( - extractedFieldsDetector -> { - explain(task, request, extractedFieldsDetector, listener); - }, - listener::onFailure - )); + extractedFieldsDetectorFactory.createFromSource( + request.getConfig(), + ActionListener.wrap( + extractedFieldsDetector -> explain(task, request, extractedFieldsDetector, listener), + listener::onFailure) + ); } private void explain(Task task, PutDataFrameAnalyticsAction.Request request, ExtractedFieldsDetector extractedFieldsDetector, diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java index b760f6f02399b..14130402a0baa 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java @@ -276,8 +276,7 @@ private void getStartContext(String id, ActionListener finalListen new SourceDestValidator(clusterService.state(), indexNameExpressionResolver).check(startContext.config); // Validate extraction is possible - boolean isTaskRestarting = startContext.startingState != DataFrameAnalyticsTask.StartingState.FIRST_TIME; - new ExtractedFieldsDetectorFactory(client).createFromSource(startContext.config, isTaskRestarting, ActionListener.wrap( + new ExtractedFieldsDetectorFactory(client).createFromSource(startContext.config, ActionListener.wrap( extractedFieldsDetector -> { startContext.extractedFields = extractedFieldsDetector.detect().v1(); toValidateDestEmptyListener.onResponse(startContext); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndex.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndex.java index a369bc7d0b09a..7996f21da6453 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndex.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndex.java @@ -28,8 +28,11 @@ import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; +import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsDest; +import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.time.Clock; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -81,7 +84,7 @@ public static void createDestinationIndex(Client client, } private static void prepareCreateIndexRequest(Client client, Clock clock, DataFrameAnalyticsConfig config, - ActionListener listener) { + ActionListener listener) { AtomicReference settingsHolder = new AtomicReference<>(); ActionListener> mappingsListener = ActionListener.wrap( @@ -102,12 +105,13 @@ private static void prepareCreateIndexRequest(Client client, Clock clock, DataFr listener::onFailure ); - GetSettingsRequest getSettingsRequest = new GetSettingsRequest(); - getSettingsRequest.indices(config.getSource().getIndex()); - getSettingsRequest.indicesOptions(IndicesOptions.lenientExpandOpen()); - getSettingsRequest.names(PRESERVED_SETTINGS); - ClientHelper.executeWithHeadersAsync(config.getHeaders(), ML_ORIGIN, client, GetSettingsAction.INSTANCE, - getSettingsRequest, getSettingsResponseListener); + GetSettingsRequest getSettingsRequest = + new GetSettingsRequest() + .indices(config.getSource().getIndex()) + .indicesOptions(IndicesOptions.lenientExpandOpen()) + .names(PRESERVED_SETTINGS); + ClientHelper.executeWithHeadersAsync( + config.getHeaders(), ML_ORIGIN, client, GetSettingsAction.INSTANCE, getSettingsRequest, getSettingsResponseListener); } private static CreateIndexRequest createIndexRequest(Clock clock, DataFrameAnalyticsConfig config, Settings settings, @@ -118,8 +122,11 @@ private static CreateIndexRequest createIndexRequest(Clock clock, DataFrameAnaly String destinationIndex = config.getDest().getIndex(); String type = mappings.keysIt().next(); Map mappingsAsMap = mappings.valuesIt().next().sourceAsMap(); - addProperties(mappingsAsMap); - addMetaData(mappingsAsMap, config.getId(), clock); + Map properties = getOrPutDefault(mappingsAsMap, PROPERTIES, HashMap::new); + checkResultsFieldIsNotPresentInProperties(config, properties); + properties.putAll(createAdditionalMappings(config, Collections.unmodifiableMap(properties))); + Map metadata = getOrPutDefault(mappingsAsMap, META, HashMap::new); + metadata.putAll(createMetaData(config.getId(), clock)); return new CreateIndexRequest(destinationIndex, settings).mapping(type, mappingsAsMap); } @@ -153,17 +160,28 @@ private static Integer findMaxSettingValue(GetSettingsResponse settingsResponse, return maxValue; } - private static void addProperties(Map mappingsAsMap) { - Map properties = getOrPutDefault(mappingsAsMap, PROPERTIES, HashMap::new); + private static Map createAdditionalMappings(DataFrameAnalyticsConfig config, Map mappingsProperties) { + Map properties = new HashMap<>(); properties.put(ID_COPY, Map.of("type", "keyword")); + for (Map.Entry entry + : config.getAnalysis().getExplicitlyMappedFields(config.getDest().getResultsField()).entrySet()) { + String destFieldPath = entry.getKey(); + String sourceFieldPath = entry.getValue(); + Object sourceFieldMapping = mappingsProperties.get(sourceFieldPath); + if (sourceFieldMapping != null) { + properties.put(destFieldPath, sourceFieldMapping); + } + } + return properties; } - private static void addMetaData(Map mappingsAsMap, String analyticsId, Clock clock) { - Map metadata = getOrPutDefault(mappingsAsMap, META, HashMap::new); + private static Map createMetaData(String analyticsId, Clock clock) { + Map metadata = new HashMap<>(); metadata.put(CREATION_DATE_MILLIS, clock.millis()); metadata.put(CREATED_BY, "data-frame-analytics"); metadata.put(VERSION, Map.of(CREATED, Version.CURRENT)); metadata.put(ANALYTICS, analyticsId); + return metadata; } @SuppressWarnings("unchecked") @@ -176,17 +194,42 @@ private static V getOrPutDefault(Map map, K key, Supplier v return value; } - public static void updateMappingsToDestIndex(Client client, DataFrameAnalyticsConfig analyticsConfig, GetIndexResponse getIndexResponse, + @SuppressWarnings("unchecked") + public static void updateMappingsToDestIndex(Client client, DataFrameAnalyticsConfig config, GetIndexResponse getIndexResponse, ActionListener listener) { // We have validated the destination index should match a single index assert getIndexResponse.indices().length == 1; - Map addedMappings = Map.of(PROPERTIES, Map.of(ID_COPY, Map.of("type", "keyword"))); + // Fetch mappings from destination index + Map destMappingsAsMap = getIndexResponse.mappings().valuesIt().next().sourceAsMap(); + Map destPropertiesAsMap = + (Map)destMappingsAsMap.getOrDefault(PROPERTIES, Collections.emptyMap()); + + // Verify that the results field does not exist in the dest index + checkResultsFieldIsNotPresentInProperties(config, destPropertiesAsMap); + + // Determine mappings to be added to the destination index + Map addedMappings = + Map.of(PROPERTIES, createAdditionalMappings(config, Collections.unmodifiableMap(destPropertiesAsMap))); - PutMappingRequest putMappingRequest = new PutMappingRequest(getIndexResponse.indices()); - putMappingRequest.source(addedMappings); - ClientHelper.executeWithHeadersAsync(analyticsConfig.getHeaders(), ML_ORIGIN, client, PutMappingAction.INSTANCE, - putMappingRequest, listener); + // Add the mappings to the destination index + PutMappingRequest putMappingRequest = + new PutMappingRequest(getIndexResponse.indices()) + .source(addedMappings); + ClientHelper.executeWithHeadersAsync( + config.getHeaders(), ML_ORIGIN, client, PutMappingAction.INSTANCE, putMappingRequest, listener); + } + + private static void checkResultsFieldIsNotPresentInProperties(DataFrameAnalyticsConfig config, Map properties) { + String resultsField = config.getDest().getResultsField(); + if (properties.containsKey(resultsField)) { + throw ExceptionsHelper.badRequestException( + "A field that matches the {}.{} [{}] already exists; please set a different {}", + DataFrameAnalyticsConfig.DEST.getPreferredName(), + DataFrameAnalyticsDest.RESULTS_FIELD.getPreferredName(), + resultsField, + DataFrameAnalyticsDest.RESULTS_FIELD.getPreferredName()); + } } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java index 8e89113be7eba..6bcd22997fbdb 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java @@ -76,7 +76,7 @@ public void execute(DataFrameAnalyticsTask task, DataFrameAnalyticsState current // The task has fully reindexed the documents and we should continue on with our analyses case ANALYZING: LOGGER.debug("[{}] Reassigning job that was analyzing", config.getId()); - startAnalytics(task, config, true); + startAnalytics(task, config); break; // If we are already at REINDEXING, we are not 100% sure if we reindexed ALL the docs. // We will delete the destination index, recreate, reindex @@ -124,7 +124,7 @@ private void executeStartingJob(DataFrameAnalyticsTask task, DataFrameAnalyticsC )); break; case RESUMING_ANALYZING: - startAnalytics(task, config, true); + startAnalytics(task, config); break; case FINISHED: default: @@ -168,7 +168,7 @@ private void reindexDataframeAndStartAnalysis(DataFrameAnalyticsTask task, DataF auditor.info( config.getId(), Messages.getMessage(Messages.DATA_FRAME_ANALYTICS_AUDIT_FINISHED_REINDEXING, config.getDest().getIndex())); - startAnalytics(task, config, false); + startAnalytics(task, config); }, error -> task.updateState(DataFrameAnalyticsState.FAILED, error.getMessage()) ); @@ -223,7 +223,7 @@ private void reindexDataframeAndStartAnalysis(DataFrameAnalyticsTask task, DataF new GetIndexRequest().indices(config.getDest().getIndex()), destIndexListener); } - private void startAnalytics(DataFrameAnalyticsTask task, DataFrameAnalyticsConfig config, boolean isTaskRestarting) { + private void startAnalytics(DataFrameAnalyticsTask task, DataFrameAnalyticsConfig config) { // Ensure we mark reindexing is finished for the case we are recovering a task that had finished reindexing task.setReindexingFinished(); @@ -249,7 +249,7 @@ private void startAnalytics(DataFrameAnalyticsTask task, DataFrameAnalyticsConfi // TODO This could fail with errors. In that case we get stuck with the copied index. // We could delete the index in case of failure or we could try building the factory before reindexing // to catch the error early on. - DataFrameDataExtractorFactory.createForDestinationIndex(client, config, isTaskRestarting, dataExtractorFactoryListener); + DataFrameDataExtractorFactory.createForDestinationIndex(client, config, dataExtractorFactoryListener); } public void stop(DataFrameAnalyticsTask task) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractor.java index f8a4348d4891d..7448a1af6eb99 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractor.java @@ -267,10 +267,7 @@ private SearchRequestBuilder buildDataSummarySearchRequestBuilder() { } public Set getCategoricalFields(DataFrameAnalysis analysis) { - return context.extractedFields.getAllFields().stream() - .filter(extractedField -> analysis.getAllowedCategoricalTypes(extractedField.getName()).containsAll(extractedField.getTypes())) - .map(ExtractedField::getName) - .collect(Collectors.toUnmodifiableSet()); + return ExtractedFieldsDetector.getCategoricalFields(context.extractedFields, analysis); } public static class DataSummary { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java index c7d27805c3b4e..3243d92bf77b6 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java @@ -31,8 +31,8 @@ public class DataFrameDataExtractorFactory { private final boolean includeRowsWithMissingValues; private DataFrameDataExtractorFactory(Client client, String analyticsId, List indices, QueryBuilder sourceQuery, - ExtractedFields extractedFields, Map headers, - boolean includeRowsWithMissingValues) { + ExtractedFields extractedFields, Map headers, + boolean includeRowsWithMissingValues) { this.client = Objects.requireNonNull(client); this.analyticsId = Objects.requireNonNull(analyticsId); this.indices = Objects.requireNonNull(indices); @@ -100,15 +100,13 @@ public static DataFrameDataExtractorFactory createForSourceIndices(Client client * * @param client ES Client used to make calls against the cluster * @param config The config from which to create the extractor factory - * @param isTaskRestarting Whether the task is restarting * @param listener The listener to notify on creation or failure */ public static void createForDestinationIndex(Client client, DataFrameAnalyticsConfig config, - boolean isTaskRestarting, ActionListener listener) { ExtractedFieldsDetectorFactory extractedFieldsDetectorFactory = new ExtractedFieldsDetectorFactory(client); - extractedFieldsDetectorFactory.createFromDest(config, isTaskRestarting, ActionListener.wrap( + extractedFieldsDetectorFactory.createFromDest(config, ActionListener.wrap( extractedFieldsDetector -> { ExtractedFields extractedFields = extractedFieldsDetector.detect().v1(); DataFrameDataExtractorFactory extractorFactory = new DataFrameDataExtractorFactory(client, config.getId(), diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java index 6897263ee0638..ba33d63ec3a9d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetector.java @@ -17,7 +17,7 @@ import org.elasticsearch.index.mapper.BooleanFieldMapper; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; -import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsDest; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.DataFrameAnalysis; import org.elasticsearch.xpack.core.ml.dataframe.analyses.RequiredField; import org.elasticsearch.xpack.core.ml.dataframe.analyses.Types; import org.elasticsearch.xpack.core.ml.dataframe.explain.FieldSelection; @@ -53,16 +53,14 @@ public class ExtractedFieldsDetector { private final String[] index; private final DataFrameAnalyticsConfig config; - private final boolean isTaskRestarting; private final int docValueFieldsLimit; private final FieldCapabilitiesResponse fieldCapabilitiesResponse; private final Map fieldCardinalities; - ExtractedFieldsDetector(String[] index, DataFrameAnalyticsConfig config, boolean isTaskRestarting, int docValueFieldsLimit, + ExtractedFieldsDetector(String[] index, DataFrameAnalyticsConfig config, int docValueFieldsLimit, FieldCapabilitiesResponse fieldCapabilitiesResponse, Map fieldCardinalities) { this.index = Objects.requireNonNull(index); this.config = Objects.requireNonNull(config); - this.isTaskRestarting = isTaskRestarting; this.docValueFieldsLimit = docValueFieldsLimit; this.fieldCapabilitiesResponse = Objects.requireNonNull(fieldCapabilitiesResponse); this.fieldCardinalities = Objects.requireNonNull(fieldCardinalities); @@ -83,7 +81,6 @@ public Tuple> detect() { private Set getIncludedFields(Set fieldSelection) { Set fields = new TreeSet<>(fieldCapabilitiesResponse.get().keySet()); fields.removeAll(IGNORE_FIELDS); - checkResultsFieldIsNotPresent(); removeFieldsUnderResultsField(fields); applySourceFiltering(fields); FetchSourceContext analyzedFields = config.getAnalyzedFields(); @@ -115,24 +112,6 @@ private void removeFieldsUnderResultsField(Set fields) { fields.removeIf(field -> field.startsWith(resultsField + ".")); } - private void checkResultsFieldIsNotPresent() { - // If the task is restarting we do not mind the index containing the results field, we will overwrite all docs - if (isTaskRestarting) { - return; - } - - String resultsField = config.getDest().getResultsField(); - Map indexToFieldCaps = fieldCapabilitiesResponse.getField(resultsField); - if (indexToFieldCaps != null && indexToFieldCaps.isEmpty() == false) { - throw ExceptionsHelper.badRequestException( - "A field that matches the {}.{} [{}] already exists; please set a different {}", - DataFrameAnalyticsConfig.DEST.getPreferredName(), - DataFrameAnalyticsDest.RESULTS_FIELD.getPreferredName(), - resultsField, - DataFrameAnalyticsDest.RESULTS_FIELD.getPreferredName()); - } - } - private void applySourceFiltering(Set fields) { Iterator fieldsIterator = fields.iterator(); while (fieldsIterator.hasNext()) { @@ -395,7 +374,7 @@ private ExtractedFields fetchBooleanFieldsAsIntegers(ExtractedFields extractedFi private void addIncludedFields(ExtractedFields extractedFields, Set fieldSelection) { Set requiredFields = config.getAnalysis().getRequiredFields().stream().map(RequiredField::getName) .collect(Collectors.toSet()); - Set categoricalFields = getCategoricalFields(extractedFields); + Set categoricalFields = getCategoricalFields(extractedFields, config.getAnalysis()); for (ExtractedField includedField : extractedFields.getAllFields()) { FieldSelection.FeatureType featureType = categoricalFields.contains(includedField.getName()) ? FieldSelection.FeatureType.CATEGORICAL : FieldSelection.FeatureType.NUMERICAL; @@ -404,9 +383,9 @@ private void addIncludedFields(ExtractedFields extractedFields, Set getCategoricalFields(ExtractedFields extractedFields) { + static Set getCategoricalFields(ExtractedFields extractedFields, DataFrameAnalysis analysis) { return extractedFields.getAllFields().stream() - .filter(extractedField -> config.getAnalysis().getAllowedCategoricalTypes(extractedField.getName()) + .filter(extractedField -> analysis.getAllowedCategoricalTypes(extractedField.getName()) .containsAll(extractedField.getTypes())) .map(ExtractedField::getName) .collect(Collectors.toUnmodifiableSet()); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java index c44555921cf38..b2d9122ef5eb8 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java @@ -49,26 +49,24 @@ public ExtractedFieldsDetectorFactory(Client client) { this.client = Objects.requireNonNull(client); } - public void createFromSource(DataFrameAnalyticsConfig config, boolean isTaskRestarting, - ActionListener listener) { - create(config.getSource().getIndex(), config, isTaskRestarting, listener); + public void createFromSource(DataFrameAnalyticsConfig config, ActionListener listener) { + create(config.getSource().getIndex(), config, listener); } - public void createFromDest(DataFrameAnalyticsConfig config, boolean isTaskRestarting, - ActionListener listener) { - create(new String[] {config.getDest().getIndex()}, config, isTaskRestarting, listener); + public void createFromDest(DataFrameAnalyticsConfig config, ActionListener listener) { + create(new String[] {config.getDest().getIndex()}, config, listener); } - private void create(String[] index, DataFrameAnalyticsConfig config, boolean isTaskRestarting, - ActionListener listener) { + private void create(String[] index, DataFrameAnalyticsConfig config, ActionListener listener) { AtomicInteger docValueFieldsLimitHolder = new AtomicInteger(); AtomicReference fieldCapsResponseHolder = new AtomicReference<>(); // Step 4. Create cardinality by field map and build detector ActionListener> fieldCardinalitiesHandler = ActionListener.wrap( fieldCardinalities -> { - ExtractedFieldsDetector detector = new ExtractedFieldsDetector(index, config, isTaskRestarting, - docValueFieldsLimitHolder.get(), fieldCapsResponseHolder.get(), fieldCardinalities); + ExtractedFieldsDetector detector = + new ExtractedFieldsDetector( + index, config, docValueFieldsLimitHolder.get(), fieldCapsResponseHolder.get(), fieldCardinalities); listener.onResponse(detector); }, listener::onFailure diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndexTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndexTests.java index 950d5997a5a35..7cd00f68e4b8f 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndexTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsIndexTests.java @@ -5,18 +5,22 @@ */ package org.elasticsearch.xpack.ml.dataframe; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.create.CreateIndexAction; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; -import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; +import org.elasticsearch.action.admin.indices.get.GetIndexResponse; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsAction; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingAction; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.action.admin.indices.settings.get.GetSettingsAction; import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest; import org.elasticsearch.action.admin.indices.settings.get.GetSettingsResponse; import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MappingMetaData; @@ -30,8 +34,14 @@ import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsDest; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsSource; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.Classification; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.DataFrameAnalysis; import org.elasticsearch.xpack.core.ml.dataframe.analyses.OutlierDetection; +import org.elasticsearch.xpack.core.ml.dataframe.analyses.Regression; +import org.junit.Assert; +import org.junit.Before; import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; import java.io.IOException; import java.time.Clock; @@ -41,13 +51,19 @@ import java.util.Map; import static org.elasticsearch.common.xcontent.support.XContentMapValues.extractValue; +import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; public class DataFrameAnalyticsIndexTests extends ESTestCase { @@ -55,13 +71,7 @@ public class DataFrameAnalyticsIndexTests extends ESTestCase { private static final String ANALYTICS_ID = "some-analytics-id"; private static final String[] SOURCE_INDEX = new String[] {"source-index"}; private static final String DEST_INDEX = "dest-index"; - private static final DataFrameAnalyticsConfig ANALYTICS_CONFIG = - new DataFrameAnalyticsConfig.Builder() - .setId(ANALYTICS_ID) - .setSource(new DataFrameAnalyticsSource(SOURCE_INDEX, null, null)) - .setDest(new DataFrameAnalyticsDest(DEST_INDEX, null)) - .setAnalysis(new OutlierDetection.Builder().build()) - .build(); + private static final String DEPENDENT_VARIABLE = "dep_var"; private static final int CURRENT_TIME_MILLIS = 123456789; private static final String CREATED_BY = "data-frame-analytics"; @@ -69,18 +79,17 @@ public class DataFrameAnalyticsIndexTests extends ESTestCase { private Client client = mock(Client.class); private Clock clock = Clock.fixed(Instant.ofEpochMilli(123456789L), ZoneId.systemDefault()); - public void testCreateDestinationIndex() throws IOException { + @Before + public void setUpMocks() { when(client.threadPool()).thenReturn(threadPool); when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); + } + + private Map testCreateDestinationIndex(DataFrameAnalysis analysis) throws IOException { + DataFrameAnalyticsConfig config = createConfig(analysis); ArgumentCaptor createIndexRequestCaptor = ArgumentCaptor.forClass(CreateIndexRequest.class); - doAnswer( - invocationOnMock -> { - @SuppressWarnings("unchecked") - ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; - listener.onResponse(null); - return null; - }) + doAnswer(callListenerOnResponse(null)) .when(client).execute(eq(CreateIndexAction.INSTANCE), createIndexRequestCaptor.capture(), any()); Settings index1Settings = Settings.builder() @@ -104,19 +113,19 @@ public void testCreateDestinationIndex() throws IOException { GetSettingsResponse getSettingsResponse = new GetSettingsResponse(indexToSettings.build(), ImmutableOpenMap.of()); - doAnswer( - invocationOnMock -> { - @SuppressWarnings("unchecked") - ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; - listener.onResponse(getSettingsResponse); - return null; - } - ).when(client).execute(eq(GetSettingsAction.INSTANCE), getSettingsRequestCaptor.capture(), any()); + doAnswer(callListenerOnResponse(getSettingsResponse)) + .when(client).execute(eq(GetSettingsAction.INSTANCE), getSettingsRequestCaptor.capture(), any()); - Map index1Mappings = Map.of("properties", Map.of("field_1", "field_1_mappings", "field_2", "field_2_mappings")); + Map index1Mappings = + Map.of( + "properties", + Map.of("field_1", "field_1_mappings", "field_2", "field_2_mappings", DEPENDENT_VARIABLE, Map.of("type", "integer"))); MappingMetaData index1MappingMetaData = new MappingMetaData("_doc", index1Mappings); - Map index2Mappings = Map.of("properties", Map.of("field_1", "field_1_mappings", "field_2", "field_2_mappings")); + Map index2Mappings = + Map.of( + "properties", + Map.of("field_1", "field_1_mappings", "field_2", "field_2_mappings", DEPENDENT_VARIABLE, Map.of("type", "integer"))); MappingMetaData index2MappingMetaData = new MappingMetaData("_doc", index2Mappings); ImmutableOpenMap.Builder mappings = ImmutableOpenMap.builder(); @@ -125,19 +134,13 @@ public void testCreateDestinationIndex() throws IOException { GetMappingsResponse getMappingsResponse = new GetMappingsResponse(mappings.build()); - doAnswer( - invocationOnMock -> { - @SuppressWarnings("unchecked") - ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; - listener.onResponse(getMappingsResponse); - return null; - } - ).when(client).execute(eq(GetMappingsAction.INSTANCE), getMappingsRequestCaptor.capture(), any()); + doAnswer(callListenerOnResponse(getMappingsResponse)) + .when(client).execute(eq(GetMappingsAction.INSTANCE), getMappingsRequestCaptor.capture(), any()); DataFrameAnalyticsIndex.createDestinationIndex( client, clock, - ANALYTICS_CONFIG, + config, ActionListener.wrap( response -> {}, e -> fail(e.getMessage()))); @@ -166,6 +169,141 @@ public void testCreateDestinationIndex() throws IOException { assertThat(extractValue("_doc._meta.analytics", map), equalTo(ANALYTICS_ID)); assertThat(extractValue("_doc._meta.creation_date_in_millis", map), equalTo(CURRENT_TIME_MILLIS)); assertThat(extractValue("_doc._meta.created_by", map), equalTo(CREATED_BY)); + return map; } } + + public void testCreateDestinationIndex_OutlierDetection() throws IOException { + testCreateDestinationIndex(new OutlierDetection.Builder().build()); + } + + public void testCreateDestinationIndex_Regression() throws IOException { + Map map = testCreateDestinationIndex(new Regression(DEPENDENT_VARIABLE)); + assertThat(extractValue("_doc.properties.ml.dep_var_prediction.type", map), equalTo("integer")); + } + + public void testCreateDestinationIndex_Classification() throws IOException { + Map map = testCreateDestinationIndex(new Classification(DEPENDENT_VARIABLE)); + assertThat(extractValue("_doc.properties.ml.dep_var_prediction.type", map), equalTo("integer")); + assertThat(extractValue("_doc.properties.ml.top_classes.class_name.type", map), equalTo("integer")); + } + + public void testCreateDestinationIndex_ResultsFieldsExistsInSourceIndex() { + DataFrameAnalyticsConfig config = createConfig(new OutlierDetection.Builder().build()); + + GetSettingsResponse getSettingsResponse = new GetSettingsResponse(ImmutableOpenMap.of(), ImmutableOpenMap.of()); + + ImmutableOpenMap.Builder mappings = ImmutableOpenMap.builder(); + mappings.put("", new MappingMetaData("_doc", Map.of("properties", Map.of("ml", "some-mapping")))); + GetMappingsResponse getMappingsResponse = new GetMappingsResponse(mappings.build()); + + doAnswer(callListenerOnResponse(getSettingsResponse)).when(client).execute(eq(GetSettingsAction.INSTANCE), any(), any()); + doAnswer(callListenerOnResponse(getMappingsResponse)).when(client).execute(eq(GetMappingsAction.INSTANCE), any(), any()); + + DataFrameAnalyticsIndex.createDestinationIndex( + client, + clock, + config, + ActionListener.wrap( + response -> fail("should not succeed"), + e -> assertThat( + e.getMessage(), + equalTo("A field that matches the dest.results_field [ml] already exists; please set a different results_field")) + ) + ); + } + + private Map testUpdateMappingsToDestIndex(DataFrameAnalysis analysis, + Map properties) throws IOException { + DataFrameAnalyticsConfig config = createConfig(analysis); + + ImmutableOpenMap.Builder mappings = ImmutableOpenMap.builder(); + mappings.put("", new MappingMetaData("_doc", Map.of("properties", properties))); + GetIndexResponse getIndexResponse = + new GetIndexResponse( + new String[] { DEST_INDEX }, mappings.build(), ImmutableOpenMap.of(), ImmutableOpenMap.of(), ImmutableOpenMap.of()); + + ArgumentCaptor putMappingRequestCaptor = ArgumentCaptor.forClass(PutMappingRequest.class); + + doAnswer(callListenerOnResponse(new AcknowledgedResponse(true))) + .when(client).execute(eq(PutMappingAction.INSTANCE), putMappingRequestCaptor.capture(), any()); + + DataFrameAnalyticsIndex.updateMappingsToDestIndex( + client, + config, + getIndexResponse, + ActionListener.wrap( + response -> assertThat(response.isAcknowledged(), is(true)), + e -> fail(e.getMessage()) + ) + ); + + verify(client, atLeastOnce()).threadPool(); + verify(client).execute(eq(PutMappingAction.INSTANCE), any(), any()); + verifyNoMoreInteractions(client); + + PutMappingRequest putMappingRequest = putMappingRequestCaptor.getValue(); + assertThat(putMappingRequest.indices(), arrayContaining(DEST_INDEX)); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, putMappingRequest.source())) { + Map map = parser.map(); + assertThat(extractValue("properties.ml__id_copy.type", map), equalTo("keyword")); + return map; + } + } + + public void testUpdateMappingsToDestIndex_OutlierDetection() throws IOException { + testUpdateMappingsToDestIndex(new OutlierDetection.Builder().build(), Map.of(DEPENDENT_VARIABLE, Map.of("type", "integer"))); + } + + public void testUpdateMappingsToDestIndex_Regression() throws IOException { + Map map = + testUpdateMappingsToDestIndex(new Regression(DEPENDENT_VARIABLE), Map.of(DEPENDENT_VARIABLE, Map.of("type", "integer"))); + assertThat(extractValue("properties.ml.dep_var_prediction.type", map), equalTo("integer")); + } + + public void testUpdateMappingsToDestIndex_Classification() throws IOException { + Map map = + testUpdateMappingsToDestIndex(new Classification(DEPENDENT_VARIABLE), Map.of(DEPENDENT_VARIABLE, Map.of("type", "integer"))); + assertThat(extractValue("properties.ml.dep_var_prediction.type", map), equalTo("integer")); + assertThat(extractValue("properties.ml.top_classes.class_name.type", map), equalTo("integer")); + } + + public void testUpdateMappingsToDestIndex_ResultsFieldsExistsInSourceIndex() { + DataFrameAnalyticsConfig config = createConfig(new OutlierDetection.Builder().build()); + + ImmutableOpenMap.Builder mappings = ImmutableOpenMap.builder(); + mappings.put("", new MappingMetaData("_doc", Map.of("properties", Map.of("ml", "some-mapping")))); + GetIndexResponse getIndexResponse = + new GetIndexResponse( + new String[] { DEST_INDEX }, mappings.build(), ImmutableOpenMap.of(), ImmutableOpenMap.of(), ImmutableOpenMap.of()); + + ElasticsearchStatusException e = + expectThrows( + ElasticsearchStatusException.class, + () -> DataFrameAnalyticsIndex.updateMappingsToDestIndex( + client, config, getIndexResponse, ActionListener.wrap(Assert::fail))); + assertThat( + e.getMessage(), + equalTo("A field that matches the dest.results_field [ml] already exists; please set a different results_field")); + + verifyZeroInteractions(client); + } + + private static Answer callListenerOnResponse(Response response) { + return invocationOnMock -> { + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + listener.onResponse(response); + return null; + }; + } + + private static DataFrameAnalyticsConfig createConfig(DataFrameAnalysis analysis) { + return new DataFrameAnalyticsConfig.Builder() + .setId(ANALYTICS_ID) + .setSource(new DataFrameAnalyticsSource(SOURCE_INDEX, null, null)) + .setDest(new DataFrameAnalyticsDest(DEST_INDEX, null)) + .setAnalysis(analysis) + .build(); + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java index 6a273a72b254f..47776b85ae8db 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java @@ -54,7 +54,7 @@ public void testDetect_GivenFloatField() { .addAggregatableField("some_float", "float").build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); @@ -72,7 +72,7 @@ public void testDetect_GivenNumericFieldWithMultipleTypes() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); @@ -90,7 +90,7 @@ public void testDetect_GivenOutlierDetectionAndNonNumericField() { .addAggregatableField("some_keyword", "keyword").build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No compatible fields could be detected in index [source_index]." + @@ -102,7 +102,7 @@ public void testDetect_GivenOutlierDetectionAndFieldWithNumericAndNonNumericType .addAggregatableField("indecisive_field", "float", "keyword").build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No compatible fields could be detected in index [source_index]. " + @@ -118,7 +118,7 @@ public void testDetect_GivenOutlierDetectionAndMultipleFields() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); @@ -147,7 +147,7 @@ public void testDetect_GivenRegressionAndMultipleFields() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("foo"), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("foo"), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); @@ -174,7 +174,7 @@ public void testDetect_GivenRegressionAndRequiredFieldMissing() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("foo"), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("foo"), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("required field [foo] is missing; analysis requires fields [foo]")); @@ -190,7 +190,7 @@ public void testDetect_GivenRegressionAndRequiredFieldExcluded() { analyzedFields = new FetchSourceContext(true, new String[0], new String[] {"foo"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("foo"), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("foo"), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("required field [foo] is missing; analysis requires fields [foo]")); @@ -206,7 +206,7 @@ public void testDetect_GivenRegressionAndRequiredFieldNotIncluded() { analyzedFields = new FetchSourceContext(true, new String[] {"some_float", "some_keyword"}, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("foo"), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("foo"), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("required field [foo] is missing; analysis requires fields [foo]")); @@ -220,7 +220,7 @@ public void testDetect_GivenFieldIsBothIncludedAndExcluded() { analyzedFields = new FetchSourceContext(true, new String[] {"foo", "bar"}, new String[] {"foo"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); @@ -241,7 +241,7 @@ public void testDetect_GivenFieldIsNotIncludedAndIsExcluded() { analyzedFields = new FetchSourceContext(true, new String[] {"foo"}, new String[] {"bar"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); @@ -263,7 +263,7 @@ public void testDetect_GivenRegressionAndRequiredFieldHasInvalidType() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("foo"), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("foo"), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("invalid types [keyword] for required field [foo]; " + @@ -279,7 +279,7 @@ public void testDetect_GivenClassificationAndRequiredFieldHasInvalidType() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildClassificationConfig("some_float"), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildClassificationConfig("some_float"), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("invalid types [float] for required field [some_float]; " + @@ -294,7 +294,7 @@ public void testDetect_GivenClassificationAndDependentVariableHasInvalidCardinal .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector(SOURCE_INDEX, - buildClassificationConfig("some_keyword"), false, 100, fieldCapabilities, Collections.singletonMap("some_keyword", 3L)); + buildClassificationConfig("some_keyword"), 100, fieldCapabilities, Collections.singletonMap("some_keyword", 3L)); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("Field [some_keyword] must have at most [2] distinct values but there were at least [3]")); @@ -305,7 +305,7 @@ public void testDetect_GivenIgnoredField() { .addAggregatableField("_id", "float").build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No compatible fields could be detected in index [source_index]. " + @@ -319,7 +319,7 @@ public void testDetect_GivenIncludedIgnoredField() { analyzedFields = new FetchSourceContext(true, new String[]{"_id"}, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No field [_id] could be detected")); @@ -332,7 +332,7 @@ public void testDetect_GivenExcludedFieldIsMissing() { analyzedFields = new FetchSourceContext(true, new String[]{"*"}, new String[] {"bar"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No field [bar] could be detected")); @@ -346,7 +346,7 @@ public void testDetect_GivenExcludedFieldIsUnsupported() { analyzedFields = new FetchSourceContext(true, null, new String[] {"categorical"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); @@ -377,7 +377,7 @@ public void testDetect_ShouldSortFieldsAlphabetically() { FieldCapabilitiesResponse fieldCapabilities = mockFieldCapsResponseBuilder.build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) @@ -394,7 +394,7 @@ public void testDetect_GivenIncludeWithMissingField() { analyzedFields = new FetchSourceContext(true, new String[]{"your_field1", "my*"}, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No field [your_field1] could be detected")); @@ -409,7 +409,7 @@ public void testDetect_GivenExcludeAllValidFields() { analyzedFields = new FetchSourceContext(true, new String[0], new String[]{"my_*"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No compatible fields could be detected in index [source_index]. " + "Supported types are [boolean, byte, double, float, half_float, integer, long, scaled_float, short].")); @@ -425,7 +425,7 @@ public void testDetect_GivenInclusionsAndExclusions() { analyzedFields = new FetchSourceContext(true, new String[]{"your*", "my_*"}, new String[]{"*nope"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) @@ -450,7 +450,7 @@ public void testDetect_GivenIncludedFieldHasUnsupportedType() { analyzedFields = new FetchSourceContext(true, new String[]{"your*", "my_*"}, new String[]{"*nope"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("field [your_keyword] has unsupported type [keyword]. " + @@ -458,22 +458,6 @@ public void testDetect_GivenIncludedFieldHasUnsupportedType() { } public void testDetect_GivenIndexContainsResultsField() { - FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() - .addAggregatableField(RESULTS_FIELD, "float") - .addAggregatableField("my_field1", "float") - .addAggregatableField("your_field2", "float") - .addAggregatableField("your_keyword", "keyword") - .build(); - - ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); - - assertThat(e.getMessage(), equalTo("A field that matches the dest.results_field [ml] already exists; " + - "please set a different results_field")); - } - - public void testDetect_GivenIndexContainsResultsFieldAndTaskIsRestarting() { FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() .addAggregatableField(RESULTS_FIELD + ".outlier_score", "float") .addAggregatableField("my_field1", "float") @@ -482,7 +466,7 @@ public void testDetect_GivenIndexContainsResultsFieldAndTaskIsRestarting() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), true, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) @@ -498,23 +482,6 @@ public void testDetect_GivenIndexContainsResultsFieldAndTaskIsRestarting() { } public void testDetect_GivenIncludedResultsField() { - FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() - .addAggregatableField(RESULTS_FIELD, "float") - .addAggregatableField("my_field1", "float") - .addAggregatableField("your_field2", "float") - .addAggregatableField("your_keyword", "keyword") - .build(); - analyzedFields = new FetchSourceContext(true, new String[]{RESULTS_FIELD}, new String[0]); - - ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); - - assertThat(e.getMessage(), equalTo("A field that matches the dest.results_field [ml] already exists; " + - "please set a different results_field")); - } - - public void testDetect_GivenIncludedResultsFieldAndTaskIsRestarting() { FieldCapabilitiesResponse fieldCapabilities = new MockFieldCapsResponseBuilder() .addAggregatableField(RESULTS_FIELD + ".outlier_score", "float") .addAggregatableField("my_field1", "float") @@ -524,7 +491,7 @@ public void testDetect_GivenIncludedResultsFieldAndTaskIsRestarting() { analyzedFields = new FetchSourceContext(true, new String[]{RESULTS_FIELD}, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), true, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, extractedFieldsDetector::detect); assertThat(e.getMessage(), equalTo("No field [ml] could be detected")); @@ -539,7 +506,7 @@ public void testDetect_GivenLessFieldsThanDocValuesLimit() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), true, 4, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 4, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) @@ -558,7 +525,7 @@ public void testDetect_GivenEqualFieldsToDocValuesLimit() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), true, 3, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 3, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) @@ -577,7 +544,7 @@ public void testDetect_GivenMoreFieldsThanDocValuesLimit() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), true, 2, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 2, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List extractedFieldNames = fieldExtraction.v1().getAllFields().stream().map(ExtractedField::getName) @@ -594,7 +561,7 @@ private void testDetect_GivenBooleanField(DataFrameAnalyticsConfig config, boole .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, config, false, 100, fieldCapabilities, config.getAnalysis().getFieldCardinalityLimits()); + SOURCE_INDEX, config, 100, fieldCapabilities, config.getAnalysis().getFieldCardinalityLimits()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); @@ -650,7 +617,7 @@ public void testDetect_GivenMultiFields() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("a_float"), true, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("a_float"), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); assertThat(fieldExtraction.v1().getAllFields(), hasSize(5)); @@ -681,7 +648,7 @@ public void testDetect_GivenMultiFieldAndParentIsRequired() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildClassificationConfig("field_1"), true, 100, fieldCapabilities, Collections.singletonMap("field_1", 2L)); + SOURCE_INDEX, buildClassificationConfig("field_1"), 100, fieldCapabilities, Collections.singletonMap("field_1", 2L)); Tuple> fieldExtraction = extractedFieldsDetector.detect(); assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); @@ -705,7 +672,7 @@ public void testDetect_GivenMultiFieldAndMultiFieldIsRequired() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildClassificationConfig("field_1.keyword"), true, 100, fieldCapabilities, + SOURCE_INDEX, buildClassificationConfig("field_1.keyword"), 100, fieldCapabilities, Collections.singletonMap("field_1.keyword", 2L)); Tuple> fieldExtraction = extractedFieldsDetector.detect(); @@ -732,7 +699,7 @@ public void testDetect_GivenSeveralMultiFields_ShouldPickFirstSorted() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("field_2"), true, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("field_2"), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); @@ -758,7 +725,7 @@ public void testDetect_GivenMultiFields_OverDocValueLimit() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("field_2"), true, 0, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("field_2"), 0, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); @@ -783,7 +750,7 @@ public void testDetect_GivenParentAndMultiFieldBothAggregatable() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("field_2.double"), true, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("field_2.double"), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); @@ -808,7 +775,7 @@ public void testDetect_GivenParentAndMultiFieldNoneAggregatable() { .build(); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("field_2"), true, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("field_2"), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); @@ -833,7 +800,7 @@ public void testDetect_GivenMultiFields_AndExplicitlyIncludedFields() { analyzedFields = new FetchSourceContext(true, new String[] { "field_1", "field_2" }, new String[0]); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildRegressionConfig("field_2"), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildRegressionConfig("field_2"), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); assertThat(fieldExtraction.v1().getAllFields(), hasSize(2)); @@ -858,7 +825,7 @@ public void testDetect_GivenSourceFilteringWithIncludes() { sourceFiltering = new FetchSourceContext(true, new String[] {"field_1*"}, null); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); @@ -881,7 +848,7 @@ public void testDetect_GivenSourceFilteringWithExcludes() { sourceFiltering = new FetchSourceContext(true, null, new String[] {"field_1*"}); ExtractedFieldsDetector extractedFieldsDetector = new ExtractedFieldsDetector( - SOURCE_INDEX, buildOutlierDetectionConfig(), false, 100, fieldCapabilities, Collections.emptyMap()); + SOURCE_INDEX, buildOutlierDetectionConfig(), 100, fieldCapabilities, Collections.emptyMap()); Tuple> fieldExtraction = extractedFieldsDetector.detect(); List allFields = fieldExtraction.v1().getAllFields(); From 3abcd6373250e873ee0f71885a4d633dd8295c1d Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 20 Dec 2019 12:31:46 +0100 Subject: [PATCH 301/686] Adapt InternalComposite serialization after backport (#50352) This commit adapts the version checks of the InternalComposite serialization after the backport of #50272. Note that this pr disables bwc tests since #50272 is not merged yet. --- build.gradle | 4 ++-- .../aggregations/bucket/composite/InternalComposite.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index b03bb7e5a3794..e465a21a09842 100644 --- a/build.gradle +++ b/build.gradle @@ -205,8 +205,8 @@ task verifyVersions { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = true -final String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */ +boolean bwc_tests_enabled = false +final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/50272" /* place a PR link here when committing bwc changes */ if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java index d3d4c34953216..503c1780d22f0 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/InternalComposite.java @@ -80,7 +80,7 @@ public InternalComposite(StreamInput in) throws IOException { this.reverseMuls = in.readIntArray(); this.buckets = in.readList((input) -> new InternalBucket(input, sourceNames, formats, reverseMuls)); this.afterKey = in.readOptionalWriteable(CompositeKey::new); - this.earlyTerminated = in.getVersion().onOrAfter(Version.V_8_0_0) ? in.readBoolean() : false; + this.earlyTerminated = in.getVersion().onOrAfter(Version.V_7_6_0) ? in.readBoolean() : false; } @Override @@ -93,7 +93,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeIntArray(reverseMuls); out.writeList(buckets); out.writeOptionalWriteable(afterKey); - if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { out.writeBoolean(earlyTerminated); } } From 5ed3bd17df8e87586c44ae38395e67ba18dd6908 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Fri, 20 Dec 2019 13:53:11 +0100 Subject: [PATCH 302/686] Remove accidentally added license files (#50370) As license infos and sha files belong to the licenses/ folder, these files seem to have been added accidentally some time ago. --- plugins/analysis-icu/bin/licenses/icu4j-59.1.jar.sha1 | 1 - .../lucene-analyzers-icu-7.0.0-snapshot-ad2cb77.jar.sha1 | 1 - 2 files changed, 2 deletions(-) delete mode 100644 plugins/analysis-icu/bin/licenses/icu4j-59.1.jar.sha1 delete mode 100644 plugins/analysis-icu/bin/licenses/lucene-analyzers-icu-7.0.0-snapshot-ad2cb77.jar.sha1 diff --git a/plugins/analysis-icu/bin/licenses/icu4j-59.1.jar.sha1 b/plugins/analysis-icu/bin/licenses/icu4j-59.1.jar.sha1 deleted file mode 100644 index 5401f914f5853..0000000000000 --- a/plugins/analysis-icu/bin/licenses/icu4j-59.1.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -6f06e820cf4c8968bbbaae66ae0b33f6a256b57f \ No newline at end of file diff --git a/plugins/analysis-icu/bin/licenses/lucene-analyzers-icu-7.0.0-snapshot-ad2cb77.jar.sha1 b/plugins/analysis-icu/bin/licenses/lucene-analyzers-icu-7.0.0-snapshot-ad2cb77.jar.sha1 deleted file mode 100644 index 0c08e240dbf47..0000000000000 --- a/plugins/analysis-icu/bin/licenses/lucene-analyzers-icu-7.0.0-snapshot-ad2cb77.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -f90e2fe9e8ff1be65a800e719d2a25cd0a09cced \ No newline at end of file From c0ec3ef640c3c5c7c2d5e55b9634038fe8dff4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Fri, 20 Dec 2019 14:00:21 +0100 Subject: [PATCH 303/686] Fix accuracy metric (#50310) --- .../classification/AccuracyMetric.java | 97 ++++---- .../client/MachineLearningIT.java | 14 +- .../AccuracyMetricResultTests.java | 8 +- .../evaluation/EvaluationMetric.java | 2 +- .../evaluation/classification/Accuracy.java | 212 +++++++++++------- .../MulticlassConfusionMatrix.java | 87 ++++--- .../evaluation/classification/Precision.java | 33 +-- .../evaluation/classification/Recall.java | 21 +- .../classification/AccuracyResultTests.java | 10 +- .../classification/AccuracyTests.java | 147 ++++++++---- .../MulticlassConfusionMatrixTests.java | 44 ++-- .../ClassificationEvaluationIT.java | 65 ++++-- .../ml/integration/ClassificationIT.java | 10 +- .../test/ml/evaluate_data_frame.yml | 17 +- 14 files changed, 472 insertions(+), 295 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetric.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetric.java index 4db165be06caa..151783499e46b 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetric.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetric.java @@ -20,6 +20,7 @@ import org.elasticsearch.client.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContent; @@ -35,10 +36,25 @@ import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; /** - * {@link AccuracyMetric} is a metric that answers the question: - * "What fraction of examples have been classified correctly by the classifier?" + * {@link AccuracyMetric} is a metric that answers the following two questions: * - * equation: accuracy = 1/n * Σ(y == y´) + * 1. What is the fraction of documents for which predicted class equals the actual class? + * + * equation: overall_accuracy = 1/n * Σ(y == y') + * where: n = total number of documents + * y = document's actual class + * y' = document's predicted class + * + * 2. For any given class X, what is the fraction of documents for which either + * a) both actual and predicted class are equal to X (true positives) + * or + * b) both actual and predicted class are not equal to X (true negatives) + * + * equation: accuracy(X) = 1/n * (TP(X) + TN(X)) + * where: X = class being examined + * n = total number of documents + * TP(X) = number of true positives wrt X + * TN(X) = number of true negatives wrt X */ public class AccuracyMetric implements EvaluationMetric { @@ -78,15 +94,15 @@ public int hashCode() { public static class Result implements EvaluationMetric.Result { - private static final ParseField ACTUAL_CLASSES = new ParseField("actual_classes"); + private static final ParseField CLASSES = new ParseField("classes"); private static final ParseField OVERALL_ACCURACY = new ParseField("overall_accuracy"); @SuppressWarnings("unchecked") private static final ConstructingObjectParser PARSER = - new ConstructingObjectParser<>("accuracy_result", true, a -> new Result((List) a[0], (double) a[1])); + new ConstructingObjectParser<>("accuracy_result", true, a -> new Result((List) a[0], (double) a[1])); static { - PARSER.declareObjectArray(constructorArg(), ActualClass.PARSER, ACTUAL_CLASSES); + PARSER.declareObjectArray(constructorArg(), PerClassResult.PARSER, CLASSES); PARSER.declareDouble(constructorArg(), OVERALL_ACCURACY); } @@ -94,13 +110,13 @@ public static Result fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } - /** List of actual classes. */ - private final List actualClasses; - /** Fraction of documents predicted correctly. */ + /** List of per-class results. */ + private final List classes; + /** Fraction of documents for which predicted class equals the actual class. */ private final double overallAccuracy; - public Result(List actualClasses, double overallAccuracy) { - this.actualClasses = Collections.unmodifiableList(Objects.requireNonNull(actualClasses)); + public Result(List classes, double overallAccuracy) { + this.classes = Collections.unmodifiableList(Objects.requireNonNull(classes)); this.overallAccuracy = overallAccuracy; } @@ -109,8 +125,8 @@ public String getMetricName() { return NAME; } - public List getActualClasses() { - return actualClasses; + public List getClasses() { + return classes; } public double getOverallAccuracy() { @@ -120,7 +136,7 @@ public double getOverallAccuracy() { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(ACTUAL_CLASSES.getPreferredName(), actualClasses); + builder.field(CLASSES.getPreferredName(), classes); builder.field(OVERALL_ACCURACY.getPreferredName(), overallAccuracy); builder.endObject(); return builder; @@ -131,52 +147,42 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Result that = (Result) o; - return Objects.equals(this.actualClasses, that.actualClasses) + return Objects.equals(this.classes, that.classes) && this.overallAccuracy == that.overallAccuracy; } @Override public int hashCode() { - return Objects.hash(actualClasses, overallAccuracy); + return Objects.hash(classes, overallAccuracy); } } - public static class ActualClass implements ToXContentObject { + public static class PerClassResult implements ToXContentObject { - private static final ParseField ACTUAL_CLASS = new ParseField("actual_class"); - private static final ParseField ACTUAL_CLASS_DOC_COUNT = new ParseField("actual_class_doc_count"); + private static final ParseField CLASS_NAME = new ParseField("class_name"); private static final ParseField ACCURACY = new ParseField("accuracy"); @SuppressWarnings("unchecked") - private static final ConstructingObjectParser PARSER = - new ConstructingObjectParser<>("accuracy_actual_class", true, a -> new ActualClass((String) a[0], (long) a[1], (double) a[2])); + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("accuracy_per_class_result", true, a -> new PerClassResult((String) a[0], (double) a[1])); static { - PARSER.declareString(constructorArg(), ACTUAL_CLASS); - PARSER.declareLong(constructorArg(), ACTUAL_CLASS_DOC_COUNT); + PARSER.declareString(constructorArg(), CLASS_NAME); PARSER.declareDouble(constructorArg(), ACCURACY); } - /** Name of the actual class. */ - private final String actualClass; - /** Number of documents (examples) belonging to the {code actualClass} class. */ - private final long actualClassDocCount; - /** Fraction of documents belonging to the {code actualClass} class predicted correctly. */ + /** Name of the class. */ + private final String className; + /** Fraction of documents that are either true positives or true negatives wrt {@code className}. */ private final double accuracy; - public ActualClass( - String actualClass, long actualClassDocCount, double accuracy) { - this.actualClass = Objects.requireNonNull(actualClass); - this.actualClassDocCount = actualClassDocCount; + public PerClassResult(String className, double accuracy) { + this.className = Objects.requireNonNull(className); this.accuracy = accuracy; } - public String getActualClass() { - return actualClass; - } - - public long getActualClassDocCount() { - return actualClassDocCount; + public String getClassName() { + return className; } public double getAccuracy() { @@ -186,8 +192,7 @@ public double getAccuracy() { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(ACTUAL_CLASS.getPreferredName(), actualClass); - builder.field(ACTUAL_CLASS_DOC_COUNT.getPreferredName(), actualClassDocCount); + builder.field(CLASS_NAME.getPreferredName(), className); builder.field(ACCURACY.getPreferredName(), accuracy); builder.endObject(); return builder; @@ -197,15 +202,19 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - ActualClass that = (ActualClass) o; - return Objects.equals(this.actualClass, that.actualClass) - && this.actualClassDocCount == that.actualClassDocCount + PerClassResult that = (PerClassResult) o; + return Objects.equals(this.className, that.className) && this.accuracy == that.accuracy; } @Override public int hashCode() { - return Objects.hash(actualClass, actualClassDocCount, accuracy); + return Objects.hash(className, accuracy); + } + + @Override + public String toString() { + return Strings.toString(this); } } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index 4aee48dff13a5..443c337d089fa 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -1819,15 +1819,15 @@ public void testEvaluateDataFrame_Classification() throws IOException { AccuracyMetric.Result accuracyResult = evaluateDataFrameResponse.getMetricByName(AccuracyMetric.NAME); assertThat(accuracyResult.getMetricName(), equalTo(AccuracyMetric.NAME)); assertThat( - accuracyResult.getActualClasses(), + accuracyResult.getClasses(), equalTo( List.of( - // 3 out of 5 examples labeled as "cat" were classified correctly - new AccuracyMetric.ActualClass("cat", 5, 0.6), - // 3 out of 4 examples labeled as "dog" were classified correctly - new AccuracyMetric.ActualClass("dog", 4, 0.75), - // no examples labeled as "ant" were classified correctly - new AccuracyMetric.ActualClass("ant", 1, 0.0)))); + // 9 out of 10 examples were classified correctly + new AccuracyMetric.PerClassResult("ant", 0.9), + // 6 out of 10 examples were classified correctly + new AccuracyMetric.PerClassResult("cat", 0.6), + // 8 out of 10 examples were classified correctly + new AccuracyMetric.PerClassResult("dog", 0.8)))); assertThat(accuracyResult.getOverallAccuracy(), equalTo(0.6)); // 6 out of 10 examples were classified correctly } { // Precision diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetricResultTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetricResultTests.java index df48ef3123dd1..8758cea86c451 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetricResultTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/evaluation/classification/AccuracyMetricResultTests.java @@ -19,7 +19,7 @@ package org.elasticsearch.client.ml.dataframe.evaluation.classification; import org.elasticsearch.client.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; -import org.elasticsearch.client.ml.dataframe.evaluation.classification.AccuracyMetric.ActualClass; +import org.elasticsearch.client.ml.dataframe.evaluation.classification.AccuracyMetric.PerClassResult; import org.elasticsearch.client.ml.dataframe.evaluation.classification.AccuracyMetric.Result; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; @@ -41,13 +41,13 @@ protected NamedXContentRegistry xContentRegistry() { public static Result randomResult() { int numClasses = randomIntBetween(2, 100); List classNames = Stream.generate(() -> randomAlphaOfLength(10)).limit(numClasses).collect(Collectors.toList()); - List actualClasses = new ArrayList<>(numClasses); + List classes = new ArrayList<>(numClasses); for (int i = 0; i < numClasses; i++) { double accuracy = randomDoubleBetween(0.0, 1.0, true); - actualClasses.add(new ActualClass(classNames.get(i), randomNonNegativeLong(), accuracy)); + classes.add(new PerClassResult(classNames.get(i), accuracy)); } double overallAccuracy = randomDoubleBetween(0.0, 1.0, true); - return new Result(actualClasses, overallAccuracy); + return new Result(classes, overallAccuracy); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java index 36bf7634cb43f..8a106175ace91 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java @@ -44,5 +44,5 @@ public interface EvaluationMetric extends ToXContentObject, NamedWriteable { * Gets the evaluation result for this metric. * @return {@code Optional.empty()} if the result is not available yet, {@code Optional.of(result)} otherwise */ - Optional getResult(); + Optional getResult(); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java index 8e7b8b6066932..c6636329a65d9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; @@ -20,7 +21,6 @@ import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; -import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetric; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -39,22 +39,36 @@ import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider.registeredMetricName; /** - * {@link Accuracy} is a metric that answers the question: - * "What fraction of examples have been classified correctly by the classifier?" + * {@link Accuracy} is a metric that answers the following two questions: * - * equation: accuracy = 1/n * Σ(y == y´) + * 1. What is the fraction of documents for which predicted class equals the actual class? + * + * equation: overall_accuracy = 1/n * Σ(y == y') + * where: n = total number of documents + * y = document's actual class + * y' = document's predicted class + * + * 2. For any given class X, what is the fraction of documents for which either + * a) both actual and predicted class are equal to X (true positives) + * or + * b) both actual and predicted class are not equal to X (true negatives) + * + * equation: accuracy(X) = 1/n * (TP(X) + TN(X)) + * where: X = class being examined + * n = total number of documents + * TP(X) = number of true positives wrt X + * TN(X) = number of true negatives wrt X */ public class Accuracy implements EvaluationMetric { public static final ParseField NAME = new ParseField("accuracy"); + static final String OVERALL_ACCURACY_AGG_NAME = "classification_overall_accuracy"; + private static final String PAINLESS_TEMPLATE = "doc[''{0}''].value == doc[''{1}''].value"; - private static final String CLASSES_AGG_NAME = "classification_classes"; - private static final String PER_CLASS_ACCURACY_AGG_NAME = "classification_per_class_accuracy"; - private static final String OVERALL_ACCURACY_AGG_NAME = "classification_overall_accuracy"; - private static String buildScript(Object...args) { - return new MessageFormat(PAINLESS_TEMPLATE, Locale.ROOT).format(args); + private static Script buildScript(Object...args) { + return new Script(new MessageFormat(PAINLESS_TEMPLATE, Locale.ROOT).format(args)); } private static final ObjectParser PARSER = new ObjectParser<>(NAME.getPreferredName(), true, Accuracy::new); @@ -63,11 +77,20 @@ public static Accuracy fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } - private EvaluationMetricResult result; + private static final int MAX_CLASSES_CARDINALITY = 1000; - public Accuracy() {} + private final MulticlassConfusionMatrix matrix; + private final SetOnce actualField = new SetOnce<>(); + private final SetOnce overallAccuracy = new SetOnce<>(); + private final SetOnce result = new SetOnce<>(); - public Accuracy(StreamInput in) throws IOException {} + public Accuracy() { + this.matrix = new MulticlassConfusionMatrix(MAX_CLASSES_CARDINALITY, NAME.getPreferredName() + "_"); + } + + public Accuracy(StreamInput in) throws IOException { + this.matrix = new MulticlassConfusionMatrix(in); + } @Override public String getWriteableName() { @@ -81,43 +104,79 @@ public String getName() { @Override public final Tuple, List> aggs(String actualField, String predictedField) { - if (result != null) { - return Tuple.tuple(List.of(), List.of()); + // Store given {@code actualField} for the purpose of generating error message in {@code process}. + this.actualField.trySet(actualField); + List aggs = new ArrayList<>(); + List pipelineAggs = new ArrayList<>(); + if (overallAccuracy.get() == null) { + aggs.add(AggregationBuilders.avg(OVERALL_ACCURACY_AGG_NAME).script(buildScript(actualField, predictedField))); + } + if (result.get() == null) { + Tuple, List> matrixAggs = matrix.aggs(actualField, predictedField); + aggs.addAll(matrixAggs.v1()); + pipelineAggs.addAll(matrixAggs.v2()); } - Script accuracyScript = new Script(buildScript(actualField, predictedField)); - return Tuple.tuple( - List.of( - AggregationBuilders.terms(CLASSES_AGG_NAME) - .field(actualField) - .subAggregation(AggregationBuilders.avg(PER_CLASS_ACCURACY_AGG_NAME).script(accuracyScript)), - AggregationBuilders.avg(OVERALL_ACCURACY_AGG_NAME).script(accuracyScript)), - List.of()); + return Tuple.tuple(aggs, pipelineAggs); } @Override public void process(Aggregations aggs) { - if (result != null) { - return; + if (overallAccuracy.get() == null && aggs.get(OVERALL_ACCURACY_AGG_NAME) instanceof NumericMetricsAggregation.SingleValue) { + NumericMetricsAggregation.SingleValue overallAccuracyAgg = aggs.get(OVERALL_ACCURACY_AGG_NAME); + overallAccuracy.set(overallAccuracyAgg.value()); } - Terms classesAgg = aggs.get(CLASSES_AGG_NAME); - NumericMetricsAggregation.SingleValue overallAccuracyAgg = aggs.get(OVERALL_ACCURACY_AGG_NAME); - List actualClasses = new ArrayList<>(classesAgg.getBuckets().size()); - for (Terms.Bucket bucket : classesAgg.getBuckets()) { - String actualClass = bucket.getKeyAsString(); - long actualClassDocCount = bucket.getDocCount(); - NumericMetricsAggregation.SingleValue accuracyAgg = bucket.getAggregations().get(PER_CLASS_ACCURACY_AGG_NAME); - actualClasses.add(new ActualClass(actualClass, actualClassDocCount, accuracyAgg.value())); + matrix.process(aggs); + if (result.get() == null && matrix.getResult().isPresent()) { + if (matrix.getResult().get().getOtherActualClassCount() > 0) { + // This means there were more than {@code maxClassesCardinality} buckets. + // We cannot calculate per-class accuracy accurately, so we fail. + throw ExceptionsHelper.badRequestException( + "Cannot calculate per-class accuracy. Cardinality of field [{}] is too high", actualField.get()); + } + result.set(new Result(computePerClassAccuracy(matrix.getResult().get()), overallAccuracy.get())); } - result = new Result(actualClasses, overallAccuracyAgg.value()); } @Override - public Optional getResult() { - return Optional.ofNullable(result); + public Optional getResult() { + return Optional.ofNullable(result.get()); + } + + /** + * Computes the per-class accuracy results based on multiclass confusion matrix's result. + * Time complexity of this method is linear wrt multiclass confusion matrix size, so O(n^2) where n is the matrix dimension. + * This method is visible for testing only. + */ + static List computePerClassAccuracy(MulticlassConfusionMatrix.Result matrixResult) { + assert matrixResult.getOtherActualClassCount() == 0; + // Number of actual classes taken into account + int n = matrixResult.getConfusionMatrix().size(); + // Total number of documents taken into account + long totalDocCount = + matrixResult.getConfusionMatrix().stream().mapToLong(MulticlassConfusionMatrix.ActualClass::getActualClassDocCount).sum(); + List classes = new ArrayList<>(n); + for (int i = 0; i < n; ++i) { + String className = matrixResult.getConfusionMatrix().get(i).getActualClass(); + // Start with the assumption that all the docs were predicted correctly. + long correctDocCount = totalDocCount; + for (int j = 0; j < n; ++j) { + if (i != j) { + // Subtract errors (false negatives) + correctDocCount -= matrixResult.getConfusionMatrix().get(i).getPredictedClasses().get(j).getCount(); + // Subtract errors (false positives) + correctDocCount -= matrixResult.getConfusionMatrix().get(j).getPredictedClasses().get(i).getCount(); + } + } + // Subtract errors (false negatives) for classes other than explicitly listed in confusion matrix + correctDocCount -= matrixResult.getConfusionMatrix().get(i).getOtherPredictedClassDocCount(); + classes.add(new PerClassResult(className, ((double)correctDocCount) / totalDocCount)); + } + return classes; } @Override public void writeTo(StreamOutput out) throws IOException { + matrix.writeTo(out); } @Override @@ -131,25 +190,26 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - return true; + Accuracy that = (Accuracy) o; + return Objects.equals(this.matrix, that.matrix); } @Override public int hashCode() { - return Objects.hashCode(NAME.getPreferredName()); + return Objects.hash(matrix); } public static class Result implements EvaluationMetricResult { - private static final ParseField ACTUAL_CLASSES = new ParseField("actual_classes"); + private static final ParseField CLASSES = new ParseField("classes"); private static final ParseField OVERALL_ACCURACY = new ParseField("overall_accuracy"); @SuppressWarnings("unchecked") private static final ConstructingObjectParser PARSER = - new ConstructingObjectParser<>("accuracy_result", true, a -> new Result((List) a[0], (double) a[1])); + new ConstructingObjectParser<>("accuracy_result", true, a -> new Result((List) a[0], (double) a[1])); static { - PARSER.declareObjectArray(constructorArg(), ActualClass.PARSER, ACTUAL_CLASSES); + PARSER.declareObjectArray(constructorArg(), PerClassResult.PARSER, CLASSES); PARSER.declareDouble(constructorArg(), OVERALL_ACCURACY); } @@ -157,18 +217,18 @@ public static Result fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } - /** List of actual classes. */ - private final List actualClasses; - /** Fraction of documents predicted correctly. */ + /** List of per-class results. */ + private final List classes; + /** Fraction of documents for which predicted class equals the actual class. */ private final double overallAccuracy; - public Result(List actualClasses, double overallAccuracy) { - this.actualClasses = Collections.unmodifiableList(ExceptionsHelper.requireNonNull(actualClasses, ACTUAL_CLASSES)); + public Result(List classes, double overallAccuracy) { + this.classes = Collections.unmodifiableList(ExceptionsHelper.requireNonNull(classes, CLASSES)); this.overallAccuracy = overallAccuracy; } public Result(StreamInput in) throws IOException { - this.actualClasses = Collections.unmodifiableList(in.readList(ActualClass::new)); + this.classes = Collections.unmodifiableList(in.readList(PerClassResult::new)); this.overallAccuracy = in.readDouble(); } @@ -182,8 +242,8 @@ public String getMetricName() { return NAME.getPreferredName(); } - public List getActualClasses() { - return actualClasses; + public List getClasses() { + return classes; } public double getOverallAccuracy() { @@ -192,14 +252,14 @@ public double getOverallAccuracy() { @Override public void writeTo(StreamOutput out) throws IOException { - out.writeList(actualClasses); + out.writeList(classes); out.writeDouble(overallAccuracy); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(ACTUAL_CLASSES.getPreferredName(), actualClasses); + builder.field(CLASSES.getPreferredName(), classes); builder.field(OVERALL_ACCURACY.getPreferredName(), overallAccuracy); builder.endObject(); return builder; @@ -210,54 +270,47 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Result that = (Result) o; - return Objects.equals(this.actualClasses, that.actualClasses) + return Objects.equals(this.classes, that.classes) && this.overallAccuracy == that.overallAccuracy; } @Override public int hashCode() { - return Objects.hash(actualClasses, overallAccuracy); + return Objects.hash(classes, overallAccuracy); } } - public static class ActualClass implements ToXContentObject, Writeable { + public static class PerClassResult implements ToXContentObject, Writeable { - private static final ParseField ACTUAL_CLASS = new ParseField("actual_class"); - private static final ParseField ACTUAL_CLASS_DOC_COUNT = new ParseField("actual_class_doc_count"); + private static final ParseField CLASS_NAME = new ParseField("class_name"); private static final ParseField ACCURACY = new ParseField("accuracy"); @SuppressWarnings("unchecked") - private static final ConstructingObjectParser PARSER = - new ConstructingObjectParser<>("accuracy_actual_class", true, a -> new ActualClass((String) a[0], (long) a[1], (double) a[2])); + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("accuracy_per_class_result", true, a -> new PerClassResult((String) a[0], (double) a[1])); static { - PARSER.declareString(constructorArg(), ACTUAL_CLASS); - PARSER.declareLong(constructorArg(), ACTUAL_CLASS_DOC_COUNT); + PARSER.declareString(constructorArg(), CLASS_NAME); PARSER.declareDouble(constructorArg(), ACCURACY); } - /** Name of the actual class. */ - private final String actualClass; - /** Number of documents (examples) belonging to the {code actualClass} class. */ - private final long actualClassDocCount; - /** Fraction of documents belonging to the {code actualClass} class predicted correctly. */ + /** Name of the class. */ + private final String className; + /** Fraction of documents that are either true positives or true negatives wrt {@code className}. */ private final double accuracy; - public ActualClass( - String actualClass, long actualClassDocCount, double accuracy) { - this.actualClass = ExceptionsHelper.requireNonNull(actualClass, ACTUAL_CLASS); - this.actualClassDocCount = actualClassDocCount; + public PerClassResult(String className, double accuracy) { + this.className = ExceptionsHelper.requireNonNull(className, CLASS_NAME); this.accuracy = accuracy; } - public ActualClass(StreamInput in) throws IOException { - this.actualClass = in.readString(); - this.actualClassDocCount = in.readVLong(); + public PerClassResult(StreamInput in) throws IOException { + this.className = in.readString(); this.accuracy = in.readDouble(); } - public String getActualClass() { - return actualClass; + public String getClassName() { + return className; } public double getAccuracy() { @@ -266,16 +319,14 @@ public double getAccuracy() { @Override public void writeTo(StreamOutput out) throws IOException { - out.writeString(actualClass); - out.writeVLong(actualClassDocCount); + out.writeString(className); out.writeDouble(accuracy); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(ACTUAL_CLASS.getPreferredName(), actualClass); - builder.field(ACTUAL_CLASS_DOC_COUNT.getPreferredName(), actualClassDocCount); + builder.field(CLASS_NAME.getPreferredName(), className); builder.field(ACCURACY.getPreferredName(), accuracy); builder.endObject(); return builder; @@ -285,15 +336,14 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - ActualClass that = (ActualClass) o; - return Objects.equals(this.actualClass, that.actualClass) - && this.actualClassDocCount == that.actualClassDocCount + PerClassResult that = (PerClassResult) o; + return Objects.equals(this.className, that.className) && this.accuracy == that.accuracy; } @Override public int hashCode() { - return Objects.hash(actualClass, actualClassDocCount, accuracy); + return Objects.hash(className, accuracy); } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java index 7b9d524abf6f7..e5a4de1605da0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.collect.Tuple; @@ -52,13 +54,16 @@ public class MulticlassConfusionMatrix implements EvaluationMetric { public static final ParseField NAME = new ParseField("multiclass_confusion_matrix"); public static final ParseField SIZE = new ParseField("size"); + public static final ParseField AGG_NAME_PREFIX = new ParseField("agg_name_prefix"); private static final ConstructingObjectParser PARSER = createParser(); private static ConstructingObjectParser createParser() { ConstructingObjectParser parser = - new ConstructingObjectParser<>(NAME.getPreferredName(), true, args -> new MulticlassConfusionMatrix((Integer) args[0])); + new ConstructingObjectParser<>( + NAME.getPreferredName(), true, args -> new MulticlassConfusionMatrix((Integer) args[0], (String) args[1])); parser.declareInt(optionalConstructorArg(), SIZE); + parser.declareString(optionalConstructorArg(), AGG_NAME_PREFIX); return parser; } @@ -66,31 +71,39 @@ public static MulticlassConfusionMatrix fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } - private static final String STEP_1_AGGREGATE_BY_ACTUAL_CLASS = NAME.getPreferredName() + "_step_1_by_actual_class"; - private static final String STEP_2_AGGREGATE_BY_ACTUAL_CLASS = NAME.getPreferredName() + "_step_2_by_actual_class"; - private static final String STEP_2_AGGREGATE_BY_PREDICTED_CLASS = NAME.getPreferredName() + "_step_2_by_predicted_class"; - private static final String STEP_2_CARDINALITY_OF_ACTUAL_CLASS = NAME.getPreferredName() + "_step_2_cardinality_of_actual_class"; + static final String STEP_1_AGGREGATE_BY_ACTUAL_CLASS = NAME.getPreferredName() + "_step_1_by_actual_class"; + static final String STEP_2_AGGREGATE_BY_ACTUAL_CLASS = NAME.getPreferredName() + "_step_2_by_actual_class"; + static final String STEP_2_AGGREGATE_BY_PREDICTED_CLASS = NAME.getPreferredName() + "_step_2_by_predicted_class"; + static final String STEP_2_CARDINALITY_OF_ACTUAL_CLASS = NAME.getPreferredName() + "_step_2_cardinality_of_actual_class"; private static final String OTHER_BUCKET_KEY = "_other_"; + private static final String DEFAULT_AGG_NAME_PREFIX = ""; private static final int DEFAULT_SIZE = 10; private static final int MAX_SIZE = 1000; private final int size; - private List topActualClassNames; - private Result result; + private final String aggNamePrefix; + private final SetOnce> topActualClassNames = new SetOnce<>(); + private final SetOnce result = new SetOnce<>(); public MulticlassConfusionMatrix() { - this((Integer) null); + this(null, null); } - public MulticlassConfusionMatrix(@Nullable Integer size) { + public MulticlassConfusionMatrix(@Nullable Integer size, @Nullable String aggNamePrefix) { if (size != null && (size <= 0 || size > MAX_SIZE)) { throw ExceptionsHelper.badRequestException("[{}] must be an integer in [1, {}]", SIZE.getPreferredName(), MAX_SIZE); } this.size = size != null ? size : DEFAULT_SIZE; + this.aggNamePrefix = aggNamePrefix != null ? aggNamePrefix : DEFAULT_AGG_NAME_PREFIX; } public MulticlassConfusionMatrix(StreamInput in) throws IOException { this.size = in.readVInt(); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + this.aggNamePrefix = in.readString(); + } else { + this.aggNamePrefix = DEFAULT_AGG_NAME_PREFIX; + } } @Override @@ -109,30 +122,30 @@ public int getSize() { @Override public final Tuple, List> aggs(String actualField, String predictedField) { - if (topActualClassNames == null) { // This is step 1 + if (topActualClassNames.get() == null) { // This is step 1 return Tuple.tuple( List.of( - AggregationBuilders.terms(STEP_1_AGGREGATE_BY_ACTUAL_CLASS) + AggregationBuilders.terms(aggName(STEP_1_AGGREGATE_BY_ACTUAL_CLASS)) .field(actualField) .order(List.of(BucketOrder.count(false), BucketOrder.key(true))) .size(size)), List.of()); } - if (result == null) { // This is step 2 + if (result.get() == null) { // This is step 2 KeyedFilter[] keyedFiltersActual = - topActualClassNames.stream() + topActualClassNames.get().stream() .map(className -> new KeyedFilter(className, QueryBuilders.termQuery(actualField, className))) .toArray(KeyedFilter[]::new); KeyedFilter[] keyedFiltersPredicted = - topActualClassNames.stream() + topActualClassNames.get().stream() .map(className -> new KeyedFilter(className, QueryBuilders.termQuery(predictedField, className))) .toArray(KeyedFilter[]::new); return Tuple.tuple( List.of( - AggregationBuilders.cardinality(STEP_2_CARDINALITY_OF_ACTUAL_CLASS) + AggregationBuilders.cardinality(aggName(STEP_2_CARDINALITY_OF_ACTUAL_CLASS)) .field(actualField), - AggregationBuilders.filters(STEP_2_AGGREGATE_BY_ACTUAL_CLASS, keyedFiltersActual) - .subAggregation(AggregationBuilders.filters(STEP_2_AGGREGATE_BY_PREDICTED_CLASS, keyedFiltersPredicted) + AggregationBuilders.filters(aggName(STEP_2_AGGREGATE_BY_ACTUAL_CLASS), keyedFiltersActual) + .subAggregation(AggregationBuilders.filters(aggName(STEP_2_AGGREGATE_BY_PREDICTED_CLASS), keyedFiltersPredicted) .otherBucket(true) .otherBucketKey(OTHER_BUCKET_KEY))), List.of()); @@ -142,18 +155,18 @@ public final Tuple, List> a @Override public void process(Aggregations aggs) { - if (topActualClassNames == null && aggs.get(STEP_1_AGGREGATE_BY_ACTUAL_CLASS) != null) { - Terms termsAgg = aggs.get(STEP_1_AGGREGATE_BY_ACTUAL_CLASS); - topActualClassNames = termsAgg.getBuckets().stream().map(Terms.Bucket::getKeyAsString).sorted().collect(Collectors.toList()); + if (topActualClassNames.get() == null && aggs.get(aggName(STEP_1_AGGREGATE_BY_ACTUAL_CLASS)) != null) { + Terms termsAgg = aggs.get(aggName(STEP_1_AGGREGATE_BY_ACTUAL_CLASS)); + topActualClassNames.set(termsAgg.getBuckets().stream().map(Terms.Bucket::getKeyAsString).sorted().collect(Collectors.toList())); } - if (result == null && aggs.get(STEP_2_AGGREGATE_BY_ACTUAL_CLASS) != null) { - Cardinality cardinalityAgg = aggs.get(STEP_2_CARDINALITY_OF_ACTUAL_CLASS); - Filters filtersAgg = aggs.get(STEP_2_AGGREGATE_BY_ACTUAL_CLASS); + if (result.get() == null && aggs.get(aggName(STEP_2_AGGREGATE_BY_ACTUAL_CLASS)) != null) { + Cardinality cardinalityAgg = aggs.get(aggName(STEP_2_CARDINALITY_OF_ACTUAL_CLASS)); + Filters filtersAgg = aggs.get(aggName(STEP_2_AGGREGATE_BY_ACTUAL_CLASS)); List actualClasses = new ArrayList<>(filtersAgg.getBuckets().size()); for (Filters.Bucket bucket : filtersAgg.getBuckets()) { String actualClass = bucket.getKeyAsString(); long actualClassDocCount = bucket.getDocCount(); - Filters subAgg = bucket.getAggregations().get(STEP_2_AGGREGATE_BY_PREDICTED_CLASS); + Filters subAgg = bucket.getAggregations().get(aggName(STEP_2_AGGREGATE_BY_PREDICTED_CLASS)); List predictedClasses = new ArrayList<>(); long otherPredictedClassDocCount = 0; for (Filters.Bucket subBucket : subAgg.getBuckets()) { @@ -168,18 +181,25 @@ public void process(Aggregations aggs) { predictedClasses.sort(comparing(PredictedClass::getPredictedClass)); actualClasses.add(new ActualClass(actualClass, actualClassDocCount, predictedClasses, otherPredictedClassDocCount)); } - result = new Result(actualClasses, Math.max(cardinalityAgg.getValue() - size, 0)); + result.set(new Result(actualClasses, Math.max(cardinalityAgg.getValue() - size, 0))); } } + private String aggName(String aggNameWithoutPrefix) { + return aggNamePrefix + aggNameWithoutPrefix; + } + @Override - public Optional getResult() { - return Optional.ofNullable(result); + public Optional getResult() { + return Optional.ofNullable(result.get()); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeVInt(size); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeString(aggNamePrefix); + } } @Override @@ -195,12 +215,13 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MulticlassConfusionMatrix that = (MulticlassConfusionMatrix) o; - return Objects.equals(this.size, that.size); + return this.size == that.size + && Objects.equals(this.aggNamePrefix, that.aggNamePrefix); } @Override public int hashCode() { - return Objects.hash(size); + return Objects.hash(size, aggNamePrefix); } public static class Result implements EvaluationMetricResult { @@ -334,6 +355,10 @@ public String getActualClass() { return actualClass; } + public long getActualClassDocCount() { + return actualClassDocCount; + } + public List getPredictedClasses() { return predictedClasses; } @@ -410,6 +435,10 @@ public String getPredictedClass() { return predictedClass; } + public long getCount() { + return count; + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(predictedClass); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java index dd04f23710118..73c7723c86b96 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; @@ -76,9 +77,9 @@ public static Precision fromXContent(XContentParser parser) { private static final int MAX_CLASSES_CARDINALITY = 1000; - private String actualField; - private List topActualClassNames; - private EvaluationMetricResult result; + private final SetOnce actualField = new SetOnce<>(); + private final SetOnce> topActualClassNames = new SetOnce<>(); + private final SetOnce result = new SetOnce<>(); public Precision() {} @@ -97,8 +98,8 @@ public String getName() { @Override public final Tuple, List> aggs(String actualField, String predictedField) { // Store given {@code actualField} for the purpose of generating error message in {@code process}. - this.actualField = actualField; - if (topActualClassNames == null) { // This is step 1 + this.actualField.trySet(actualField); + if (topActualClassNames.get() == null) { // This is step 1 return Tuple.tuple( List.of( AggregationBuilders.terms(ACTUAL_CLASSES_NAMES_AGG_NAME) @@ -107,9 +108,9 @@ public final Tuple, List> a .size(MAX_CLASSES_CARDINALITY)), List.of()); } - if (result == null) { // This is step 2 + if (result.get() == null) { // This is step 2 KeyedFilter[] keyedFiltersPredicted = - topActualClassNames.stream() + topActualClassNames.get().stream() .map(className -> new KeyedFilter(className, QueryBuilders.termQuery(predictedField, className))) .toArray(KeyedFilter[]::new); Script script = buildScript(actualField, predictedField); @@ -127,18 +128,18 @@ public final Tuple, List> a @Override public void process(Aggregations aggs) { - if (topActualClassNames == null && aggs.get(ACTUAL_CLASSES_NAMES_AGG_NAME) instanceof Terms) { + if (topActualClassNames.get() == null && aggs.get(ACTUAL_CLASSES_NAMES_AGG_NAME) instanceof Terms) { Terms topActualClassesAgg = aggs.get(ACTUAL_CLASSES_NAMES_AGG_NAME); if (topActualClassesAgg.getSumOfOtherDocCounts() > 0) { - // This means there were more than {@code maxClassesCardinality} buckets. + // This means there were more than {@code MAX_CLASSES_CARDINALITY} buckets. // We cannot calculate average precision accurately, so we fail. throw ExceptionsHelper.badRequestException( - "Cannot calculate average precision. Cardinality of field [{}] is too high", actualField); + "Cannot calculate average precision. Cardinality of field [{}] is too high", actualField.get()); } - topActualClassNames = - topActualClassesAgg.getBuckets().stream().map(Terms.Bucket::getKeyAsString).sorted().collect(Collectors.toList()); + topActualClassNames.set( + topActualClassesAgg.getBuckets().stream().map(Terms.Bucket::getKeyAsString).sorted().collect(Collectors.toList())); } - if (result == null && + if (result.get() == null && aggs.get(BY_PREDICTED_CLASS_AGG_NAME) instanceof Filters && aggs.get(AVG_PRECISION_AGG_NAME) instanceof NumericMetricsAggregation.SingleValue) { Filters byPredictedClassAgg = aggs.get(BY_PREDICTED_CLASS_AGG_NAME); @@ -152,13 +153,13 @@ public void process(Aggregations aggs) { classes.add(new PerClassResult(className, precision)); } } - result = new Result(classes, avgPrecisionAgg.value()); + result.set(new Result(classes, avgPrecisionAgg.value())); } } @Override - public Optional getResult() { - return Optional.ofNullable(result); + public Optional getResult() { + return Optional.ofNullable(result.get()); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java index 01bdbe6db230b..0358820cc509c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; @@ -70,8 +71,8 @@ public static Recall fromXContent(XContentParser parser) { private static final int MAX_CLASSES_CARDINALITY = 1000; - private String actualField; - private EvaluationMetricResult result; + private final SetOnce actualField = new SetOnce<>(); + private final SetOnce result = new SetOnce<>(); public Recall() {} @@ -90,8 +91,8 @@ public String getName() { @Override public final Tuple, List> aggs(String actualField, String predictedField) { // Store given {@code actualField} for the purpose of generating error message in {@code process}. - this.actualField = actualField; - if (result != null) { + this.actualField.trySet(actualField); + if (result.get() != null) { return Tuple.tuple(List.of(), List.of()); } Script script = buildScript(actualField, predictedField); @@ -109,15 +110,15 @@ public final Tuple, List> a @Override public void process(Aggregations aggs) { - if (result == null && + if (result.get() == null && aggs.get(BY_ACTUAL_CLASS_AGG_NAME) instanceof Terms && aggs.get(AVG_RECALL_AGG_NAME) instanceof NumericMetricsAggregation.SingleValue) { Terms byActualClassAgg = aggs.get(BY_ACTUAL_CLASS_AGG_NAME); if (byActualClassAgg.getSumOfOtherDocCounts() > 0) { - // This means there were more than {@code maxClassesCardinality} buckets. + // This means there were more than {@code MAX_CLASSES_CARDINALITY} buckets. // We cannot calculate average recall accurately, so we fail. throw ExceptionsHelper.badRequestException( - "Cannot calculate average recall. Cardinality of field [{}] is too high", actualField); + "Cannot calculate average recall. Cardinality of field [{}] is too high", actualField.get()); } NumericMetricsAggregation.SingleValue avgRecallAgg = aggs.get(AVG_RECALL_AGG_NAME); List classes = new ArrayList<>(byActualClassAgg.getBuckets().size()); @@ -126,13 +127,13 @@ public void process(Aggregations aggs) { NumericMetricsAggregation.SingleValue recallAgg = bucket.getAggregations().get(PER_ACTUAL_CLASS_RECALL_AGG_NAME); classes.add(new PerClassResult(className, recallAgg.value())); } - result = new Result(classes, avgRecallAgg.value()); + result.set(new Result(classes, avgRecallAgg.value())); } } @Override - public Optional getResult() { - return Optional.ofNullable(result); + public Optional getResult() { + return Optional.ofNullable(result.get()); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyResultTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyResultTests.java index 8fb4c6c02408d..176aa6e9a309b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyResultTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyResultTests.java @@ -8,9 +8,9 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.test.AbstractWireSerializingTestCase; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy.ActualClass; -import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy.Result; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.MlEvaluationNamedXContentProvider; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy.PerClassResult; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy.Result; import java.util.ArrayList; import java.util.List; @@ -22,13 +22,13 @@ public class AccuracyResultTests extends AbstractWireSerializingTestCase public static Result createRandom() { int numClasses = randomIntBetween(2, 100); List classNames = Stream.generate(() -> randomAlphaOfLength(10)).limit(numClasses).collect(Collectors.toList()); - List actualClasses = new ArrayList<>(numClasses); + List classes = new ArrayList<>(numClasses); for (int i = 0; i < numClasses; i++) { double accuracy = randomDoubleBetween(0.0, 1.0, true); - actualClasses.add(new ActualClass(classNames.get(i), randomNonNegativeLong(), accuracy)); + classes.add(new PerClassResult(classNames.get(i), accuracy)); } double overallAccuracy = randomDoubleBetween(0.0, 1.0, true); - return new Result(actualClasses, overallAccuracy); + return new Result(classes, overallAccuracy); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java index 1809f0e735125..cac591a17d303 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java @@ -5,17 +5,26 @@ */ package org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy.PerClassResult; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy.Result; import java.io.IOException; -import java.util.Arrays; import java.util.List; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockCardinality; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockFilters; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockFiltersBucket; import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockSingleValue; import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockTerms; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.MockAggregations.mockTermsBucket; +import static org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.TupleMatchers.isTuple; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; public class AccuracyTests extends AbstractSerializingTestCase { @@ -45,53 +54,109 @@ public static Accuracy createRandom() { } public void testProcess() { - Aggregations aggs = new Aggregations(Arrays.asList( - mockTerms("classification_classes"), - mockSingleValue("classification_overall_accuracy", 0.8123), - mockSingleValue("some_other_single_metric_agg", 0.2377) - )); + Aggregations aggs = new Aggregations(List.of( + mockTerms( + "accuracy_" + MulticlassConfusionMatrix.STEP_1_AGGREGATE_BY_ACTUAL_CLASS, + List.of( + mockTermsBucket("dog", new Aggregations(List.of())), + mockTermsBucket("cat", new Aggregations(List.of()))), + 100L), + mockFilters( + "accuracy_" + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_ACTUAL_CLASS, + List.of( + mockFiltersBucket( + "dog", + 30, + new Aggregations(List.of(mockFilters( + "accuracy_" + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_PREDICTED_CLASS, + List.of(mockFiltersBucket("cat", 10L), mockFiltersBucket("dog", 20L), mockFiltersBucket("_other_", 0L)))))), + mockFiltersBucket( + "cat", + 70, + new Aggregations(List.of(mockFilters( + "accuracy_" + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_PREDICTED_CLASS, + List.of(mockFiltersBucket("cat", 30L), mockFiltersBucket("dog", 40L), mockFiltersBucket("_other_", 0L)))))))), + mockCardinality("accuracy_" + MulticlassConfusionMatrix.STEP_2_CARDINALITY_OF_ACTUAL_CLASS, 1000L), + mockSingleValue(Accuracy.OVERALL_ACCURACY_AGG_NAME, 0.5))); Accuracy accuracy = new Accuracy(); accuracy.process(aggs); - assertThat(accuracy.getResult().get(), equalTo(new Accuracy.Result(List.of(), 0.8123))); + assertThat(accuracy.aggs("act", "pred"), isTuple(empty(), empty())); + + Result result = accuracy.getResult().get(); + assertThat(result.getMetricName(), equalTo(Accuracy.NAME.getPreferredName())); + assertThat( + result.getClasses(), + equalTo( + List.of( + new PerClassResult("dog", 0.5), + new PerClassResult("cat", 0.5)))); + assertThat(result.getOverallAccuracy(), equalTo(0.5)); + } + + public void testProcess_GivenCardinalityTooHigh() { + Aggregations aggs = new Aggregations(List.of( + mockTerms( + "accuracy_" + MulticlassConfusionMatrix.STEP_1_AGGREGATE_BY_ACTUAL_CLASS, + List.of( + mockTermsBucket("dog", new Aggregations(List.of())), + mockTermsBucket("cat", new Aggregations(List.of()))), + 100L), + mockFilters( + "accuracy_" + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_ACTUAL_CLASS, + List.of( + mockFiltersBucket( + "dog", + 30, + new Aggregations(List.of(mockFilters( + "accuracy_" + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_PREDICTED_CLASS, + List.of(mockFiltersBucket("cat", 10L), mockFiltersBucket("dog", 20L), mockFiltersBucket("_other_", 0L)))))), + mockFiltersBucket( + "cat", + 70, + new Aggregations(List.of(mockFilters( + "accuracy_" + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_PREDICTED_CLASS, + List.of(mockFiltersBucket("cat", 30L), mockFiltersBucket("dog", 40L), mockFiltersBucket("_other_", 0L)))))))), + mockCardinality("accuracy_" + MulticlassConfusionMatrix.STEP_2_CARDINALITY_OF_ACTUAL_CLASS, 1001L), + mockSingleValue(Accuracy.OVERALL_ACCURACY_AGG_NAME, 0.5))); + + Accuracy accuracy = new Accuracy(); + accuracy.aggs("foo", "bar"); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> accuracy.process(aggs)); + assertThat(e.getMessage(), containsString("Cardinality of field [foo] is too high")); } - public void testProcess_GivenMissingAgg() { - { - Aggregations aggs = new Aggregations(Arrays.asList( - mockTerms("classification_classes"), - mockSingleValue("some_other_single_metric_agg", 0.2377) - )); - Accuracy accuracy = new Accuracy(); - expectThrows(NullPointerException.class, () -> accuracy.process(aggs)); - } - { - Aggregations aggs = new Aggregations(Arrays.asList( - mockSingleValue("classification_overall_accuracy", 0.8123), - mockSingleValue("some_other_single_metric_agg", 0.2377) - )); - Accuracy accuracy = new Accuracy(); - expectThrows(NullPointerException.class, () -> accuracy.process(aggs)); - } + public void testComputePerClassAccuracy() { + assertThat( + Accuracy.computePerClassAccuracy( + new MulticlassConfusionMatrix.Result( + List.of( + new MulticlassConfusionMatrix.ActualClass("A", 14, List.of( + new MulticlassConfusionMatrix.PredictedClass("A", 1), + new MulticlassConfusionMatrix.PredictedClass("B", 6), + new MulticlassConfusionMatrix.PredictedClass("C", 4) + ), 3L), + new MulticlassConfusionMatrix.ActualClass("B", 20, List.of( + new MulticlassConfusionMatrix.PredictedClass("A", 5), + new MulticlassConfusionMatrix.PredictedClass("B", 3), + new MulticlassConfusionMatrix.PredictedClass("C", 9) + ), 3L), + new MulticlassConfusionMatrix.ActualClass("C", 17, List.of( + new MulticlassConfusionMatrix.PredictedClass("A", 8), + new MulticlassConfusionMatrix.PredictedClass("B", 2), + new MulticlassConfusionMatrix.PredictedClass("C", 7) + ), 0L)), + 0)), + equalTo( + List.of( + new Accuracy.PerClassResult("A", 25.0 / 51), // 13 false positives, 13 false negatives + new Accuracy.PerClassResult("B", 26.0 / 51), // 8 false positives, 17 false negatives + new Accuracy.PerClassResult("C", 28.0 / 51))) // 13 false positives, 10 false negatives + ); } - public void testProcess_GivenAggOfWrongType() { - { - Aggregations aggs = new Aggregations(Arrays.asList( - mockTerms("classification_classes"), - mockTerms("classification_overall_accuracy") - )); - Accuracy accuracy = new Accuracy(); - expectThrows(ClassCastException.class, () -> accuracy.process(aggs)); - } - { - Aggregations aggs = new Aggregations(Arrays.asList( - mockSingleValue("classification_classes", 1.0), - mockSingleValue("classification_overall_accuracy", 0.8123) - )); - Accuracy accuracy = new Accuracy(); - expectThrows(ClassCastException.class, () -> accuracy.process(aggs)); - } + public void testComputePerClassAccuracy_OtherActualClassCountIsNonZero() { + expectThrows(AssertionError.class, () -> Accuracy.computePerClassAccuracy(new MulticlassConfusionMatrix.Result(List.of(), 1))); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java index f145a06c3c894..8c02a3c2c6fc3 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.ActualClass; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.PredictedClass; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.MulticlassConfusionMatrix.Result; import java.io.IOException; import java.util.List; @@ -54,20 +55,23 @@ protected boolean supportsUnknownFields() { public static MulticlassConfusionMatrix createRandom() { Integer size = randomBoolean() ? null : randomIntBetween(1, 1000); - return new MulticlassConfusionMatrix(size); + return new MulticlassConfusionMatrix(size, null); } public void testConstructor_SizeValidationFailures() { { - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> new MulticlassConfusionMatrix(-1)); + ElasticsearchStatusException e = + expectThrows(ElasticsearchStatusException.class, () -> new MulticlassConfusionMatrix(-1, null)); assertThat(e.getMessage(), equalTo("[size] must be an integer in [1, 1000]")); } { - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> new MulticlassConfusionMatrix(0)); + ElasticsearchStatusException e = + expectThrows(ElasticsearchStatusException.class, () -> new MulticlassConfusionMatrix(0, null)); assertThat(e.getMessage(), equalTo("[size] must be an integer in [1, 1000]")); } { - ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> new MulticlassConfusionMatrix(1001)); + ElasticsearchStatusException e = + expectThrows(ElasticsearchStatusException.class, () -> new MulticlassConfusionMatrix(1001, null)); assertThat(e.getMessage(), equalTo("[size] must be an integer in [1, 1000]")); } } @@ -82,34 +86,34 @@ public void testAggs() { public void testEvaluate() { Aggregations aggs = new Aggregations(List.of( mockTerms( - "multiclass_confusion_matrix_step_1_by_actual_class", + MulticlassConfusionMatrix.STEP_1_AGGREGATE_BY_ACTUAL_CLASS, List.of( mockTermsBucket("dog", new Aggregations(List.of())), mockTermsBucket("cat", new Aggregations(List.of()))), 0L), mockFilters( - "multiclass_confusion_matrix_step_2_by_actual_class", + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_ACTUAL_CLASS, List.of( mockFiltersBucket( "dog", 30, new Aggregations(List.of(mockFilters( - "multiclass_confusion_matrix_step_2_by_predicted_class", + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_PREDICTED_CLASS, List.of(mockFiltersBucket("cat", 10L), mockFiltersBucket("dog", 20L), mockFiltersBucket("_other_", 0L)))))), mockFiltersBucket( "cat", 70, new Aggregations(List.of(mockFilters( - "multiclass_confusion_matrix_step_2_by_predicted_class", + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_PREDICTED_CLASS, List.of(mockFiltersBucket("cat", 30L), mockFiltersBucket("dog", 40L), mockFiltersBucket("_other_", 0L)))))))), - mockCardinality("multiclass_confusion_matrix_step_2_cardinality_of_actual_class", 2L))); + mockCardinality(MulticlassConfusionMatrix.STEP_2_CARDINALITY_OF_ACTUAL_CLASS, 2L))); - MulticlassConfusionMatrix confusionMatrix = new MulticlassConfusionMatrix(2); + MulticlassConfusionMatrix confusionMatrix = new MulticlassConfusionMatrix(2, null); confusionMatrix.process(aggs); assertThat(confusionMatrix.aggs("act", "pred"), isTuple(empty(), empty())); - MulticlassConfusionMatrix.Result result = (MulticlassConfusionMatrix.Result) confusionMatrix.getResult().get(); - assertThat(result.getMetricName(), equalTo("multiclass_confusion_matrix")); + Result result = confusionMatrix.getResult().get(); + assertThat(result.getMetricName(), equalTo(MulticlassConfusionMatrix.NAME.getPreferredName())); assertThat( result.getConfusionMatrix(), equalTo( @@ -122,34 +126,34 @@ public void testEvaluate() { public void testEvaluate_OtherClassesCountGreaterThanZero() { Aggregations aggs = new Aggregations(List.of( mockTerms( - "multiclass_confusion_matrix_step_1_by_actual_class", + MulticlassConfusionMatrix.STEP_1_AGGREGATE_BY_ACTUAL_CLASS, List.of( mockTermsBucket("dog", new Aggregations(List.of())), mockTermsBucket("cat", new Aggregations(List.of()))), 100L), mockFilters( - "multiclass_confusion_matrix_step_2_by_actual_class", + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_ACTUAL_CLASS, List.of( mockFiltersBucket( "dog", 30, new Aggregations(List.of(mockFilters( - "multiclass_confusion_matrix_step_2_by_predicted_class", + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_PREDICTED_CLASS, List.of(mockFiltersBucket("cat", 10L), mockFiltersBucket("dog", 20L), mockFiltersBucket("_other_", 0L)))))), mockFiltersBucket( "cat", 85, new Aggregations(List.of(mockFilters( - "multiclass_confusion_matrix_step_2_by_predicted_class", + MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_PREDICTED_CLASS, List.of(mockFiltersBucket("cat", 30L), mockFiltersBucket("dog", 40L), mockFiltersBucket("_other_", 15L)))))))), - mockCardinality("multiclass_confusion_matrix_step_2_cardinality_of_actual_class", 5L))); + mockCardinality(MulticlassConfusionMatrix.STEP_2_CARDINALITY_OF_ACTUAL_CLASS, 5L))); - MulticlassConfusionMatrix confusionMatrix = new MulticlassConfusionMatrix(2); + MulticlassConfusionMatrix confusionMatrix = new MulticlassConfusionMatrix(2, null); confusionMatrix.process(aggs); assertThat(confusionMatrix.aggs("act", "pred"), isTuple(empty(), empty())); - MulticlassConfusionMatrix.Result result = (MulticlassConfusionMatrix.Result) confusionMatrix.getResult().get(); - assertThat(result.getMetricName(), equalTo("multiclass_confusion_matrix")); + Result result = confusionMatrix.getResult().get(); + assertThat(result.getMetricName(), equalTo(MulticlassConfusionMatrix.NAME.getPreferredName())); assertThat( result.getConfusionMatrix(), equalTo( diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java index 437b2ddbf5180..da5439f6298dc 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationEvaluationIT.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction; +import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Accuracy; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Precision; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.classification.Recall; @@ -21,6 +22,8 @@ import java.util.List; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -52,10 +55,28 @@ public void testEvaluate_DefaultMetrics() { evaluateDataFrame(ANIMALS_DATA_INDEX, new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, null)); assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); - assertThat(evaluateDataFrameResponse.getMetrics(), hasSize(1)); assertThat( - evaluateDataFrameResponse.getMetrics().get(0).getMetricName(), - equalTo(MulticlassConfusionMatrix.NAME.getPreferredName())); + evaluateDataFrameResponse.getMetrics().stream().map(EvaluationMetricResult::getMetricName).collect(toList()), + contains(MulticlassConfusionMatrix.NAME.getPreferredName())); + } + + public void testEvaluate_AllMetrics() { + EvaluateDataFrameAction.Response evaluateDataFrameResponse = + evaluateDataFrame( + ANIMALS_DATA_INDEX, + new Classification( + ANIMAL_NAME_FIELD, + ANIMAL_NAME_PREDICTION_FIELD, + List.of(new Accuracy(), new MulticlassConfusionMatrix(), new Precision(), new Recall()))); + + assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); + assertThat( + evaluateDataFrameResponse.getMetrics().stream().map(EvaluationMetricResult::getMetricName).collect(toList()), + contains( + Accuracy.NAME.getPreferredName(), + MulticlassConfusionMatrix.NAME.getPreferredName(), + Precision.NAME.getPreferredName(), + Recall.NAME.getPreferredName())); } public void testEvaluate_Accuracy_KeywordField() { @@ -69,14 +90,14 @@ public void testEvaluate_Accuracy_KeywordField() { Accuracy.Result accuracyResult = (Accuracy.Result) evaluateDataFrameResponse.getMetrics().get(0); assertThat(accuracyResult.getMetricName(), equalTo(Accuracy.NAME.getPreferredName())); assertThat( - accuracyResult.getActualClasses(), + accuracyResult.getClasses(), equalTo( List.of( - new Accuracy.ActualClass("ant", 15, 1.0 / 15), - new Accuracy.ActualClass("cat", 15, 1.0 / 15), - new Accuracy.ActualClass("dog", 15, 1.0 / 15), - new Accuracy.ActualClass("fox", 15, 1.0 / 15), - new Accuracy.ActualClass("mouse", 15, 1.0 / 15)))); + new Accuracy.PerClassResult("ant", 47.0 / 75), + new Accuracy.PerClassResult("cat", 47.0 / 75), + new Accuracy.PerClassResult("dog", 47.0 / 75), + new Accuracy.PerClassResult("fox", 47.0 / 75), + new Accuracy.PerClassResult("mouse", 47.0 / 75)))); assertThat(accuracyResult.getOverallAccuracy(), equalTo(5.0 / 75)); } @@ -91,13 +112,14 @@ public void testEvaluate_Accuracy_IntegerField() { Accuracy.Result accuracyResult = (Accuracy.Result) evaluateDataFrameResponse.getMetrics().get(0); assertThat(accuracyResult.getMetricName(), equalTo(Accuracy.NAME.getPreferredName())); assertThat( - accuracyResult.getActualClasses(), - equalTo(List.of( - new Accuracy.ActualClass("1", 15, 1.0 / 15), - new Accuracy.ActualClass("2", 15, 2.0 / 15), - new Accuracy.ActualClass("3", 15, 3.0 / 15), - new Accuracy.ActualClass("4", 15, 4.0 / 15), - new Accuracy.ActualClass("5", 15, 5.0 / 15)))); + accuracyResult.getClasses(), + equalTo( + List.of( + new Accuracy.PerClassResult("1", 57.0 / 75), + new Accuracy.PerClassResult("2", 54.0 / 75), + new Accuracy.PerClassResult("3", 51.0 / 75), + new Accuracy.PerClassResult("4", 48.0 / 75), + new Accuracy.PerClassResult("5", 45.0 / 75)))); assertThat(accuracyResult.getOverallAccuracy(), equalTo(15.0 / 75)); } @@ -112,10 +134,11 @@ public void testEvaluate_Accuracy_BooleanField() { Accuracy.Result accuracyResult = (Accuracy.Result) evaluateDataFrameResponse.getMetrics().get(0); assertThat(accuracyResult.getMetricName(), equalTo(Accuracy.NAME.getPreferredName())); assertThat( - accuracyResult.getActualClasses(), - equalTo(List.of( - new Accuracy.ActualClass("true", 45, 27.0 / 45), - new Accuracy.ActualClass("false", 30, 18.0 / 30)))); + accuracyResult.getClasses(), + equalTo( + List.of( + new Accuracy.PerClassResult("false", 18.0 / 30), + new Accuracy.PerClassResult("true", 27.0 / 45)))); assertThat(accuracyResult.getOverallAccuracy(), equalTo(45.0 / 75)); } @@ -250,7 +273,7 @@ public void testEvaluate_ConfusionMatrixMetricWithUserProvidedSize() { EvaluateDataFrameAction.Response evaluateDataFrameResponse = evaluateDataFrame( ANIMALS_DATA_INDEX, - new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new MulticlassConfusionMatrix(3)))); + new Classification(ANIMAL_NAME_FIELD, ANIMAL_NAME_PREDICTION_FIELD, List.of(new MulticlassConfusionMatrix(3, null)))); assertThat(evaluateDataFrameResponse.getEvaluationName(), equalTo(Classification.NAME.getPreferredName())); assertThat(evaluateDataFrameResponse.getMetrics(), hasSize(1)); diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java index fc2739c0a70e3..978b11efb0f39 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java @@ -466,12 +466,10 @@ private void assertEvaluation(String dependentVariable, List dependentVar { // Accuracy Accuracy.Result accuracyResult = (Accuracy.Result) evaluateDataFrameResponse.getMetrics().get(0); assertThat(accuracyResult.getMetricName(), equalTo(Accuracy.NAME.getPreferredName())); - List actualClasses = accuracyResult.getActualClasses(); - assertThat( - actualClasses.stream().map(Accuracy.ActualClass::getActualClass).collect(toList()), - equalTo(dependentVariableValuesAsStrings)); - actualClasses.forEach( - actualClass -> assertThat(actualClass.getAccuracy(), allOf(greaterThanOrEqualTo(0.0), lessThanOrEqualTo(1.0)))); + for (Accuracy.PerClassResult klass : accuracyResult.getClasses()) { + assertThat(klass.getClassName(), is(in(dependentVariableValuesAsStrings))); + assertThat(klass.getAccuracy(), allOf(greaterThanOrEqualTo(0.0), lessThanOrEqualTo(1.0))); + } } { // MulticlassConfusionMatrix diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/evaluate_data_frame.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/evaluate_data_frame.yml index 95a7ef4e33218..2b16b79ac84b4 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/evaluate_data_frame.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/evaluate_data_frame.yml @@ -620,16 +620,13 @@ setup: - match: classification.accuracy: - actual_classes: - - actual_class: "cat" - actual_class_doc_count: 3 - accuracy: 0.6666666666666666 # 2 out of 3 - - actual_class: "dog" - actual_class_doc_count: 3 - accuracy: 0.6666666666666666 # 2 out of 3 - - actual_class: "mouse" - actual_class_doc_count: 2 - accuracy: 0.5 # 1 out of 2 + classes: + - class_name: "cat" + accuracy: 0.625 # 5 out of 8 + - class_name: "dog" + accuracy: 0.75 # 6 out of 8 + - class_name: "mouse" + accuracy: 0.875 # 7 out of 8 overall_accuracy: 0.625 # 5 out of 8 --- "Test classification precision": From bf5e87f5384ab3719208835f6f3d9d01cf828584 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 20 Dec 2019 14:34:56 +0100 Subject: [PATCH 304/686] reenable bwc test now that #50272 is merged (#50430) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e465a21a09842..b03bb7e5a3794 100644 --- a/build.gradle +++ b/build.gradle @@ -205,8 +205,8 @@ task verifyVersions { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = false -final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/50272" /* place a PR link here when committing bwc changes */ +boolean bwc_tests_enabled = true +final String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */ if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") From 1ab7a4b0a57d6813442526eca073740ccfcedacf Mon Sep 17 00:00:00 2001 From: Florian Kelbert Date: Fri, 20 Dec 2019 13:47:24 +0000 Subject: [PATCH 305/686] [DOCS] Fix typo in bucket sum aggregation docs (#50431) --- .../aggregations/pipeline/sum-bucket-aggregation.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/aggregations/pipeline/sum-bucket-aggregation.asciidoc b/docs/reference/aggregations/pipeline/sum-bucket-aggregation.asciidoc index 1bae8bcb5251a..81375967cfe8b 100644 --- a/docs/reference/aggregations/pipeline/sum-bucket-aggregation.asciidoc +++ b/docs/reference/aggregations/pipeline/sum-bucket-aggregation.asciidoc @@ -1,7 +1,7 @@ [[search-aggregations-pipeline-sum-bucket-aggregation]] === Sum Bucket Aggregation -A sibling pipeline aggregation which calculates the sum across all bucket of a specified metric in a sibling aggregation. +A sibling pipeline aggregation which calculates the sum across all buckets of a specified metric in a sibling aggregation. The specified metric must be numeric and the sibling aggregation must be a multi-bucket aggregation. ==== Syntax From 9162267960ee31ec0ac35a741b31df35b8c8b6d5 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 20 Dec 2019 15:43:00 +0100 Subject: [PATCH 306/686] Disable slm in AbstractWatcherIntegrationTestCase (#50422) SLM isn't required tests extending from this base class and only add noise during test suite teardown. Closes #50302 --- .../xpack/watcher/test/AbstractWatcherIntegrationTestCase.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/AbstractWatcherIntegrationTestCase.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/AbstractWatcherIntegrationTestCase.java index 0c1377ac79767..d276718ce2f7a 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/AbstractWatcherIntegrationTestCase.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/AbstractWatcherIntegrationTestCase.java @@ -117,6 +117,9 @@ protected Settings nodeSettings(int nodeOrdinal) { .put("xpack.watcher.execution.scroll.size", randomIntBetween(1, 100)) .put("xpack.watcher.watch.scroll.size", randomIntBetween(1, 100)) .put("index.lifecycle.history_index_enabled", false) + // SLM can cause timing issues during testsuite teardown: https://github.com/elastic/elasticsearch/issues/50302 + // SLM is not required for tests extending from this base class and only add noise. + .put("xpack.slm.enabled", false) .build(); } From f012958ec88174c0ee738bc56e1f21e862cfc906 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 20 Dec 2019 04:47:42 -1000 Subject: [PATCH 307/686] Geo: Switch generated GeoJson type names to camel case (#50285) (#50400) Switches generated GeoJson type names to camel case to conform to the standard. Closes #49568 --- .../ingest/processors/circle.asciidoc | 2 +- .../org/elasticsearch/common/geo/GeoJson.java | 18 +++++++++--------- .../common/geo/GeometryParserTests.java | 4 ++-- .../index/query/GeoShapeQueryBuilderTests.java | 2 +- .../index/query/ShapeQueryBuilderTests.java | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/reference/ingest/processors/circle.asciidoc b/docs/reference/ingest/processors/circle.asciidoc index 300d7763fe634..a8a6bda02f8e2 100644 --- a/docs/reference/ingest/processors/circle.asciidoc +++ b/docs/reference/ingest/processors/circle.asciidoc @@ -128,7 +128,7 @@ The response from the above index request: [30.000365257263184, 10.0] ] ], - "type": "polygon" + "type": "Polygon" } } } diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java b/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java index 0a1454a868738..5eb327588fb80 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java @@ -382,17 +382,17 @@ public static String getGeoJsonName(Geometry geometry) { return geometry.visit(new GeometryVisitor<>() { @Override public String visit(Circle circle) { - return "circle"; + return "Circle"; } @Override public String visit(GeometryCollection collection) { - return "geometrycollection"; + return "GeometryCollection"; } @Override public String visit(Line line) { - return "linestring"; + return "LineString"; } @Override @@ -402,32 +402,32 @@ public String visit(LinearRing ring) { @Override public String visit(MultiLine multiLine) { - return "multilinestring"; + return "MultiLineString"; } @Override public String visit(MultiPoint multiPoint) { - return "multipoint"; + return "MultiPoint"; } @Override public String visit(MultiPolygon multiPolygon) { - return "multipolygon"; + return "MultiPolygon"; } @Override public String visit(Point point) { - return "point"; + return "Point"; } @Override public String visit(Polygon polygon) { - return "polygon"; + return "Polygon"; } @Override public String visit(Rectangle rectangle) { - return "envelope"; + return "Envelope"; } }); } diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java index df4b47f23689f..4c53d40fb75db 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java @@ -51,7 +51,7 @@ public void testGeoJsonParsing() throws Exception { assertEquals(new Point(100, 0), format.fromXContent(parser)); XContentBuilder newGeoJson = XContentFactory.jsonBuilder(); format.toXContent(new Point(100, 10), newGeoJson, ToXContent.EMPTY_PARAMS); - assertEquals("{\"type\":\"point\",\"coordinates\":[100.0,10.0]}", Strings.toString(newGeoJson)); + assertEquals("{\"type\":\"Point\",\"coordinates\":[100.0,10.0]}", Strings.toString(newGeoJson)); } XContentBuilder pointGeoJsonWithZ = XContentFactory.jsonBuilder() @@ -148,7 +148,7 @@ public void testNullParsing() throws Exception { // if we serialize non-null value - it should be serialized as geojson format.toXContent(new Point(100, 10), newGeoJson, ToXContent.EMPTY_PARAMS); newGeoJson.endObject(); - assertEquals("{\"val\":{\"type\":\"point\",\"coordinates\":[100.0,10.0]}}", Strings.toString(newGeoJson)); + assertEquals("{\"val\":{\"type\":\"Point\",\"coordinates\":[100.0,10.0]}}", Strings.toString(newGeoJson)); newGeoJson = XContentFactory.jsonBuilder().startObject().field("val"); format.toXContent(null, newGeoJson, ToXContent.EMPTY_PARAMS); diff --git a/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java index 5eb89ccd4d454..8020083d106b6 100644 --- a/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java @@ -204,7 +204,7 @@ public void testFromJson() throws IOException { " \"geo_shape\" : {\n" + " \"location\" : {\n" + " \"shape\" : {\n" + - " \"type\" : \"envelope\",\n" + + " \"type\" : \"Envelope\",\n" + " \"coordinates\" : [ [ 13.0, 53.0 ], [ 14.0, 52.0 ] ]\n" + " },\n" + " \"relation\" : \"intersects\"\n" + diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java index 2e9cd2db3a55d..fb0d7fbbe7557 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java @@ -179,7 +179,7 @@ public void testFromJson() throws IOException { " }\n" + "}"; ShapeQueryBuilder parsed = (ShapeQueryBuilder) parseQuery(json); - checkGeneratedJson(json, parsed); + checkGeneratedJson(json.replaceAll("envelope", "Envelope"), parsed); assertEquals(json, 42.0, parsed.boost(), 0.0001); } From c793b775199a2563d09cbd9d06a06f7c9a3e4abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Fri, 20 Dec 2019 16:14:01 +0100 Subject: [PATCH 308/686] Now, that the PR with `aggName` is backported, the version can be changed to 7.6 (#50436) --- .../evaluation/classification/MulticlassConfusionMatrix.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java index e5a4de1605da0..e2762bd7d7986 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java @@ -99,7 +99,7 @@ public MulticlassConfusionMatrix(@Nullable Integer size, @Nullable String aggNam public MulticlassConfusionMatrix(StreamInput in) throws IOException { this.size = in.readVInt(); - if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { this.aggNamePrefix = in.readString(); } else { this.aggNamePrefix = DEFAULT_AGG_NAME_PREFIX; @@ -197,7 +197,7 @@ public Optional getResult() { @Override public void writeTo(StreamOutput out) throws IOException { out.writeVInt(size); - if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { out.writeString(aggNamePrefix); } } From 8b06b14f0ee87a0eaf4acb576ca1422c08cc8612 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 20 Dec 2019 11:26:42 -0500 Subject: [PATCH 309/686] [ML][Inference] updates specs with new params + docs (#50373) --- .../rest-api-spec/api/ml.delete_trained_model.json | 2 +- .../rest-api-spec/api/ml.get_trained_models.json | 10 ++++++++-- .../rest-api-spec/api/ml.get_trained_models_stats.json | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_trained_model.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_trained_model.json index edfc157646f91..01e935bd4ad59 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_trained_model.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_trained_model.json @@ -1,7 +1,7 @@ { "ml.delete_trained_model":{ "documentation":{ - "url":"TODO" + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/delete-inference.html" }, "stability":"experimental", "url":{ diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.get_trained_models.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.get_trained_models.json index 22d16a6c36941..d92c8823b7330 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.get_trained_models.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.get_trained_models.json @@ -1,7 +1,7 @@ { "ml.get_trained_models":{ "documentation":{ - "url":"TODO" + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/get-inference.html" }, "stability":"experimental", "url":{ @@ -36,9 +36,15 @@ "include_model_definition":{ "type":"boolean", "required":false, - "description":"Should the full model definition be included in the results. These definitions can be large", + "description":"Should the full model definition be included in the results. These definitions can be large. So be cautious when including them. Defaults to false.", "default":false }, + "decompress_definition": { + "type": "boolean", + "required": false, + "default": true, + "description": "Should the model definition be decompressed into valid JSON or returned in a custom compressed format. Defaults to true." + }, "from":{ "type":"int", "description":"skips a number of trained models", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.get_trained_models_stats.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.get_trained_models_stats.json index 703380c708703..c00b33f2ea9ca 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.get_trained_models_stats.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.get_trained_models_stats.json @@ -1,7 +1,7 @@ { "ml.get_trained_models_stats":{ "documentation":{ - "url":"TODO" + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/get-inference-stats.html" }, "stability":"experimental", "url":{ From 79a570f2e8decde289ca2e029f5e65a9ae357a13 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Fri, 20 Dec 2019 16:29:37 +0000 Subject: [PATCH 310/686] Make the TransportRolloverAction execute in one cluster state update (#50388) This commit makes the TransportRolloverAction more resilient, by having it execute only one cluster state update that creates the new (rollover index), rolls over the alias from the source to the target index and set the RolloverInfo on the source index. Before these 3 steps were represented as 3 chained cluster state updates, which would've seen the user manually intervene if, say, the alias rollover cluster state update (second in the chain) failed but the creation of the rollover index (first in the chain) update succeeded * Rename innerExecute to applyAliasActions Co-authored-by: Elastic Machine --- .../rollover/TransportRolloverAction.java | 112 ++++++++---------- .../metadata/MetaDataCreateIndexService.java | 2 +- .../metadata/MetaDataIndexAliasesService.java | 7 +- .../TransportRolloverActionTests.java | 13 +- .../MetaDataIndexAliasesServiceTests.java | 46 +++---- 5 files changed, 81 insertions(+), 99 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverAction.java index bc67685a976f7..9c1c9d71d6708 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverAction.java @@ -20,7 +20,6 @@ package org.elasticsearch.action.admin.indices.rollover; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.indices.alias.IndicesAliasesClusterStateUpdateRequest; import org.elasticsearch.action.admin.indices.create.CreateIndexClusterStateUpdateRequest; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction; @@ -130,7 +129,7 @@ protected void masterOperation(Task task, final RolloverRequest rolloverRequest, .docs(true); statsRequest.setParentTask(clusterService.localNode().getId(), task.getId()); client.execute(IndicesStatsAction.INSTANCE, statsRequest, - new ActionListener() { + new ActionListener<>() { @Override public void onResponse(IndicesStatsResponse statsResponse) { final Map conditionResults = evaluateConditions(rolloverRequest.getConditions().values(), @@ -141,56 +140,44 @@ public void onResponse(IndicesStatsResponse statsResponse) { new RolloverResponse(sourceIndexName, rolloverIndexName, conditionResults, true, false, false, false)); return; } - List> metConditions = rolloverRequest.getConditions().values().stream() + List> metConditions = rolloverRequest.getConditions().values().stream() .filter(condition -> conditionResults.get(condition.toString())).collect(Collectors.toList()); if (conditionResults.size() == 0 || metConditions.size() > 0) { - CreateIndexClusterStateUpdateRequest updateRequest = prepareCreateIndexRequest(unresolvedName, rolloverIndexName, - rolloverRequest); - createIndexService.createIndex(updateRequest, ActionListener.wrap(createIndexClusterStateUpdateResponse -> { - final IndicesAliasesClusterStateUpdateRequest aliasesUpdateRequest; - if (explicitWriteIndex) { - aliasesUpdateRequest = prepareRolloverAliasesWriteIndexUpdateRequest(sourceIndexName, - rolloverIndexName, rolloverRequest); - } else { - aliasesUpdateRequest = prepareRolloverAliasesUpdateRequest(sourceIndexName, - rolloverIndexName, rolloverRequest); + CreateIndexClusterStateUpdateRequest createIndexRequest = prepareCreateIndexRequest(unresolvedName, + rolloverIndexName, rolloverRequest); + clusterService.submitStateUpdateTask("rollover_index source [" + sourceIndexName + "] to target [" + + rolloverIndexName + "]", new ClusterStateUpdateTask() { + @Override + public ClusterState execute(ClusterState currentState) throws Exception { + ClusterState newState = createIndexService.applyCreateIndexRequest(currentState, createIndexRequest); + newState = indexAliasesService.applyAliasActions(newState, + rolloverAliasToNewIndex(sourceIndexName, rolloverIndexName, rolloverRequest, explicitWriteIndex)); + RolloverInfo rolloverInfo = new RolloverInfo(rolloverRequest.getAlias(), metConditions, + threadPool.absoluteTimeInMillis()); + return ClusterState.builder(newState) + .metaData(MetaData.builder(newState.metaData()) + .put(IndexMetaData.builder(newState.metaData().index(sourceIndexName)) + .putRolloverInfo(rolloverInfo))).build(); } - indexAliasesService.indicesAliases(aliasesUpdateRequest, - ActionListener.wrap(aliasClusterStateUpdateResponse -> { - if (aliasClusterStateUpdateResponse.isAcknowledged()) { - clusterService.submitStateUpdateTask("update_rollover_info", new ClusterStateUpdateTask() { - @Override - public ClusterState execute(ClusterState currentState) { - RolloverInfo rolloverInfo = new RolloverInfo(rolloverRequest.getAlias(), metConditions, - threadPool.absoluteTimeInMillis()); - return ClusterState.builder(currentState) - .metaData(MetaData.builder(currentState.metaData()) - .put(IndexMetaData.builder(currentState.metaData().index(sourceIndexName)) - .putRolloverInfo(rolloverInfo))).build(); - } - @Override - public void onFailure(String source, Exception e) { - listener.onFailure(e); - } + @Override + public void onFailure(String source, Exception e) { + listener.onFailure(e); + } - @Override - public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { - activeShardsObserver.waitForActiveShards(new String[]{rolloverIndexName}, - rolloverRequest.getCreateIndexRequest().waitForActiveShards(), - rolloverRequest.masterNodeTimeout(), - isShardsAcknowledged -> listener.onResponse(new RolloverResponse( - sourceIndexName, rolloverIndexName, conditionResults, false, true, true, - isShardsAcknowledged)), - listener::onFailure); - } - }); - } else { - listener.onResponse(new RolloverResponse(sourceIndexName, rolloverIndexName, conditionResults, - false, true, false, false)); - } - }, listener::onFailure)); - }, listener::onFailure)); + @Override + public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { + if (newState.equals(oldState) == false) { + activeShardsObserver.waitForActiveShards(new String[]{rolloverIndexName}, + rolloverRequest.getCreateIndexRequest().waitForActiveShards(), + rolloverRequest.masterNodeTimeout(), + isShardsAcknowledged -> listener.onResponse(new RolloverResponse( + sourceIndexName, rolloverIndexName, conditionResults, false, true, true, + isShardsAcknowledged)), + listener::onFailure); + } + } + }); } else { // conditions not met listener.onResponse( @@ -207,27 +194,24 @@ public void onFailure(Exception e) { ); } - static IndicesAliasesClusterStateUpdateRequest prepareRolloverAliasesUpdateRequest(String oldIndex, String newIndex, - RolloverRequest request) { - final List actions = List.of( - new AliasAction.Add(newIndex, request.getAlias(), null, null, null, null), - new AliasAction.Remove(oldIndex, request.getAlias())); - return new IndicesAliasesClusterStateUpdateRequest(actions) - .ackTimeout(request.ackTimeout()) - .masterNodeTimeout(request.masterNodeTimeout()); - } - - static IndicesAliasesClusterStateUpdateRequest prepareRolloverAliasesWriteIndexUpdateRequest(String oldIndex, String newIndex, - RolloverRequest request) { - final List actions = List.of( + /** + * Creates the alias actions to reflect the alias rollover from the old (source) index to the new (target/rolled over) index. An + * alias pointing to multiple indices will have to be an explicit write index (ie. the old index alias has is_write_index set to true) + * in which case, after the rollover, the new index will need to be the explicit write index. + */ + static List rolloverAliasToNewIndex(String oldIndex, String newIndex, RolloverRequest request, + boolean explicitWriteIndex) { + if (explicitWriteIndex) { + return List.of( new AliasAction.Add(newIndex, request.getAlias(), null, null, null, true), new AliasAction.Add(oldIndex, request.getAlias(), null, null, null, false)); - return new IndicesAliasesClusterStateUpdateRequest(actions) - .ackTimeout(request.ackTimeout()) - .masterNodeTimeout(request.masterNodeTimeout()); + } else { + return List.of( + new AliasAction.Add(newIndex, request.getAlias(), null, null, null, null), + new AliasAction.Remove(oldIndex, request.getAlias())); + } } - static String generateRolloverIndexName(String sourceIndexName, IndexNameExpressionResolver indexNameExpressionResolver) { String resolvedName = indexNameExpressionResolver.resolveDateMathExpression(sourceIndexName); final boolean isDateMath = sourceIndexName.equals(resolvedName) == false; diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java index 6a977fdaee368..9e310280636a0 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java @@ -262,7 +262,7 @@ public void onFailure(String source, Exception e) { * Handles the cluster state transition to a version that reflects the {@link CreateIndexClusterStateUpdateRequest}. * All the requested changes are firstly validated before mutating the {@link ClusterState}. */ - ClusterState applyCreateIndexRequest(ClusterState currentState, CreateIndexClusterStateUpdateRequest request) throws Exception { + public ClusterState applyCreateIndexRequest(ClusterState currentState, CreateIndexClusterStateUpdateRequest request) throws Exception { logger.trace("executing IndexCreationTask for [{}] against cluster state version [{}]", request, currentState.version()); Index createdIndex = null; String removalExtraInfo = null; diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesService.java index 84e2f512e569f..5efd4b6eae8bc 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesService.java @@ -85,12 +85,15 @@ protected ClusterStateUpdateResponse newResponse(boolean acknowledged) { @Override public ClusterState execute(ClusterState currentState) { - return innerExecute(currentState, request.actions()); + return applyAliasActions(currentState, request.actions()); } }); } - ClusterState innerExecute(ClusterState currentState, Iterable actions) { + /** + * Handles the cluster state transition to a version that reflects the provided {@link AliasAction}s. + */ + public ClusterState applyAliasActions(ClusterState currentState, Iterable actions) { List indicesToClose = new ArrayList<>(); Map indices = new HashMap<>(); try { diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverActionTests.java index 9dac4a38a36b6..54454cd9f7ecb 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverActionTests.java @@ -22,7 +22,6 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequest; -import org.elasticsearch.action.admin.indices.alias.IndicesAliasesClusterStateUpdateRequest; import org.elasticsearch.action.admin.indices.create.CreateIndexClusterStateUpdateRequest; import org.elasticsearch.action.admin.indices.stats.CommonStats; import org.elasticsearch.action.admin.indices.stats.IndexStats; @@ -219,15 +218,13 @@ public void testEvaluateWithoutMetaData() { results2.forEach((k, v) -> assertFalse(v)); } - public void testCreateUpdateAliasRequest() { + public void testRolloverAliasActions() { String sourceAlias = randomAlphaOfLength(10); String sourceIndex = randomAlphaOfLength(10); String targetIndex = randomAlphaOfLength(10); final RolloverRequest rolloverRequest = new RolloverRequest(sourceAlias, targetIndex); - final IndicesAliasesClusterStateUpdateRequest updateRequest = - TransportRolloverAction.prepareRolloverAliasesUpdateRequest(sourceIndex, targetIndex, rolloverRequest); - List actions = updateRequest.actions(); + List actions = TransportRolloverAction.rolloverAliasToNewIndex(sourceIndex, targetIndex, rolloverRequest, false); assertThat(actions, hasSize(2)); boolean foundAdd = false; boolean foundRemove = false; @@ -246,15 +243,13 @@ public void testCreateUpdateAliasRequest() { assertTrue(foundRemove); } - public void testCreateUpdateAliasRequestWithExplicitWriteIndex() { + public void testRolloverAliasActionsWithExplicitWriteIndex() { String sourceAlias = randomAlphaOfLength(10); String sourceIndex = randomAlphaOfLength(10); String targetIndex = randomAlphaOfLength(10); final RolloverRequest rolloverRequest = new RolloverRequest(sourceAlias, targetIndex); - final IndicesAliasesClusterStateUpdateRequest updateRequest = - TransportRolloverAction.prepareRolloverAliasesWriteIndexUpdateRequest(sourceIndex, targetIndex, rolloverRequest); + List actions = TransportRolloverAction.rolloverAliasToNewIndex(sourceIndex, targetIndex, rolloverRequest, true); - List actions = updateRequest.actions(); assertThat(actions, hasSize(2)); boolean foundAddWrite = false; boolean foundRemoveWrite = false; diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesServiceTests.java index 7f7f26bcfbcbb..9b320ae9f27e7 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesServiceTests.java @@ -72,7 +72,7 @@ public void testAddAndRemove() { ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), index); // Add an alias to it - ClusterState after = service.innerExecute(before, singletonList(new AliasAction.Add(index, "test", null, null, null, null))); + ClusterState after = service.applyAliasActions(before, singletonList(new AliasAction.Add(index, "test", null, null, null, null))); AliasOrIndex alias = after.metaData().getAliasAndIndexLookup().get("test"); assertNotNull(alias); assertTrue(alias.isAlias()); @@ -81,7 +81,7 @@ public void testAddAndRemove() { // Remove the alias from it while adding another one before = after; - after = service.innerExecute(before, Arrays.asList( + after = service.applyAliasActions(before, Arrays.asList( new AliasAction.Remove(index, "test"), new AliasAction.Add(index, "test_2", null, null, null, null))); assertNull(after.metaData().getAliasAndIndexLookup().get("test")); @@ -93,7 +93,7 @@ public void testAddAndRemove() { // Now just remove on its own before = after; - after = service.innerExecute(before, singletonList(new AliasAction.Remove(index, "test_2"))); + after = service.applyAliasActions(before, singletonList(new AliasAction.Remove(index, "test_2"))); assertNull(after.metaData().getAliasAndIndexLookup().get("test")); assertNull(after.metaData().getAliasAndIndexLookup().get("test_2")); assertAliasesVersionIncreased(index, before, after); @@ -109,7 +109,7 @@ public void testMultipleIndices() { before = createIndex(before, index); addActions.add(new AliasAction.Add(index, "alias-" + index, null, null, null, null)); } - final ClusterState afterAddingAliasesToAll = service.innerExecute(before, addActions); + final ClusterState afterAddingAliasesToAll = service.applyAliasActions(before, addActions); assertAliasesVersionIncreased(indices.toArray(new String[0]), before, afterAddingAliasesToAll); // now add some aliases randomly @@ -121,7 +121,7 @@ public void testMultipleIndices() { randomIndices.add(index); } } - final ClusterState afterAddingRandomAliases = service.innerExecute(afterAddingAliasesToAll, randomAddActions); + final ClusterState afterAddingRandomAliases = service.applyAliasActions(afterAddingAliasesToAll, randomAddActions); assertAliasesVersionIncreased(randomIndices.toArray(new String[0]), afterAddingAliasesToAll, afterAddingRandomAliases); assertAliasesVersionUnchanged( Sets.difference(indices, randomIndices).toArray(new String[0]), @@ -134,15 +134,15 @@ public void testChangingWriteAliasStateIncreasesAliasesVersion() { final ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), index); final ClusterState afterAddWriteAlias = - service.innerExecute(before, singletonList(new AliasAction.Add(index, "test", null, null, null, true))); + service.applyAliasActions(before, singletonList(new AliasAction.Add(index, "test", null, null, null, true))); assertAliasesVersionIncreased(index, before, afterAddWriteAlias); final ClusterState afterChangeWriteAliasToNonWriteAlias = - service.innerExecute(afterAddWriteAlias, singletonList(new AliasAction.Add(index, "test", null, null, null, false))); + service.applyAliasActions(afterAddWriteAlias, singletonList(new AliasAction.Add(index, "test", null, null, null, false))); assertAliasesVersionIncreased(index, afterAddWriteAlias, afterChangeWriteAliasToNonWriteAlias); final ClusterState afterChangeNonWriteAliasToWriteAlias = - service.innerExecute( + service.applyAliasActions( afterChangeWriteAliasToNonWriteAlias, singletonList(new AliasAction.Add(index, "test", null, null, null, true))); assertAliasesVersionIncreased(index, afterChangeWriteAliasToNonWriteAlias, afterChangeNonWriteAliasToWriteAlias); @@ -158,7 +158,7 @@ public void testAddingAliasMoreThanOnceShouldOnlyIncreaseAliasesVersionByOne() { for (int i = 0; i < length; i++) { addActions.add(new AliasAction.Add(index, "test", null, null, null, null)); } - final ClusterState afterAddingAliases = service.innerExecute(before, addActions); + final ClusterState afterAddingAliases = service.applyAliasActions(before, addActions); assertAliasesVersionIncreased(index, before, afterAddingAliases); } @@ -175,7 +175,7 @@ public void testAliasesVersionUnchangedWhenActionsAreIdempotent() { final String aliasName = randomValueOtherThanMany(v -> aliasNames.add(v) == false, () -> randomAlphaOfLength(8)); addActions.add(new AliasAction.Add(index, aliasName, null, null, null, null)); } - final ClusterState afterAddingAlias = service.innerExecute(before, addActions); + final ClusterState afterAddingAlias = service.applyAliasActions(before, addActions); // now perform a remove and add for each alias which is idempotent, the resulting aliases are unchanged final var removeAndAddActions = new ArrayList(2 * length); @@ -183,7 +183,7 @@ public void testAliasesVersionUnchangedWhenActionsAreIdempotent() { removeAndAddActions.add(new AliasAction.Remove(index, aliasName)); removeAndAddActions.add(new AliasAction.Add(index, aliasName, null, null, null, null)); } - final ClusterState afterRemoveAndAddAlias = service.innerExecute(afterAddingAlias, removeAndAddActions); + final ClusterState afterRemoveAndAddAlias = service.applyAliasActions(afterAddingAlias, removeAndAddActions); assertAliasesVersionUnchanged(index, afterAddingAlias, afterRemoveAndAddAlias); } @@ -193,7 +193,7 @@ public void testSwapIndexWithAlias() { before = createIndex(before, "test_2"); // Now remove "test" and add an alias to "test" to "test_2" in one go - ClusterState after = service.innerExecute(before, Arrays.asList( + ClusterState after = service.applyAliasActions(before, Arrays.asList( new AliasAction.Add("test_2", "test", null, null, null, null), new AliasAction.RemoveIndex("test"))); AliasOrIndex alias = after.metaData().getAliasAndIndexLookup().get("test"); @@ -208,7 +208,7 @@ public void testAddAliasToRemovedIndex() { ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), "test"); // Attempt to add an alias to "test" at the same time as we remove it - IndexNotFoundException e = expectThrows(IndexNotFoundException.class, () -> service.innerExecute(before, Arrays.asList( + IndexNotFoundException e = expectThrows(IndexNotFoundException.class, () -> service.applyAliasActions(before, Arrays.asList( new AliasAction.Add("test", "alias", null, null, null, null), new AliasAction.RemoveIndex("test")))); assertEquals("test", e.getIndex().getName()); @@ -219,7 +219,7 @@ public void testRemoveIndexTwice() { ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), "test"); // Try to remove an index twice. This should just remove the index once.... - ClusterState after = service.innerExecute(before, Arrays.asList( + ClusterState after = service.applyAliasActions(before, Arrays.asList( new AliasAction.RemoveIndex("test"), new AliasAction.RemoveIndex("test"))); assertNull(after.metaData().getAliasAndIndexLookup().get("test")); @@ -228,20 +228,20 @@ public void testRemoveIndexTwice() { public void testAddWriteOnlyWithNoExistingAliases() { ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), "test"); - ClusterState after = service.innerExecute(before, Arrays.asList( + ClusterState after = service.applyAliasActions(before, Arrays.asList( new AliasAction.Add("test", "alias", null, null, null, false))); assertFalse(after.metaData().index("test").getAliases().get("alias").writeIndex()); assertNull(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex()); assertAliasesVersionIncreased("test", before, after); - after = service.innerExecute(before, Arrays.asList( + after = service.applyAliasActions(before, Arrays.asList( new AliasAction.Add("test", "alias", null, null, null, null))); assertNull(after.metaData().index("test").getAliases().get("alias").writeIndex()); assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(), equalTo(after.metaData().index("test"))); assertAliasesVersionIncreased("test", before, after); - after = service.innerExecute(before, Arrays.asList( + after = service.applyAliasActions(before, Arrays.asList( new AliasAction.Add("test", "alias", null, null, null, true))); assertTrue(after.metaData().index("test").getAliases().get("alias").writeIndex()); assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(), @@ -258,7 +258,7 @@ public void testAddWriteOnlyWithExistingWriteIndex() { ClusterState before = ClusterState.builder(ClusterName.DEFAULT) .metaData(MetaData.builder().put(indexMetaData).put(indexMetaData2)).build(); - ClusterState after = service.innerExecute(before, Arrays.asList( + ClusterState after = service.applyAliasActions(before, Arrays.asList( new AliasAction.Add("test", "alias", null, null, null, null))); assertNull(after.metaData().index("test").getAliases().get("alias").writeIndex()); assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(), @@ -266,7 +266,7 @@ public void testAddWriteOnlyWithExistingWriteIndex() { assertAliasesVersionIncreased("test", before, after); assertAliasesVersionUnchanged("test2", before, after); - Exception exception = expectThrows(IllegalStateException.class, () -> service.innerExecute(before, Arrays.asList( + Exception exception = expectThrows(IllegalStateException.class, () -> service.applyAliasActions(before, Arrays.asList( new AliasAction.Add("test", "alias", null, null, null, true)))); assertThat(exception.getMessage(), startsWith("alias [alias] has more than one write index [")); } @@ -286,7 +286,7 @@ public void testSwapWriteOnlyIndex() { new AliasAction.Add("test2", "alias", null, null, null, true) ); Collections.shuffle(swapActions, random()); - ClusterState after = service.innerExecute(before, swapActions); + ClusterState after = service.applyAliasActions(before, swapActions); assertThat(after.metaData().index("test").getAliases().get("alias").writeIndex(), equalTo(unsetValue)); assertTrue(after.metaData().index("test2").getAliases().get("alias").writeIndex()); assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(), @@ -309,7 +309,7 @@ public void testAddWriteOnlyWithExistingNonWriteIndices() { assertNull(((AliasOrIndex.Alias) before.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex()); - ClusterState after = service.innerExecute(before, Arrays.asList( + ClusterState after = service.applyAliasActions(before, Arrays.asList( new AliasAction.Add("test3", "alias", null, null, null, true))); assertTrue(after.metaData().index("test3").getAliases().get("alias").writeIndex()); assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(), @@ -333,7 +333,7 @@ public void testAddWriteOnlyWithIndexRemoved() { assertNull(before.metaData().index("test2").getAliases().get("alias").writeIndex()); assertNull(((AliasOrIndex.Alias) before.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex()); - ClusterState after = service.innerExecute(before, Collections.singletonList(new AliasAction.RemoveIndex("test"))); + ClusterState after = service.applyAliasActions(before, Collections.singletonList(new AliasAction.RemoveIndex("test"))); assertNull(after.metaData().index("test2").getAliases().get("alias").writeIndex()); assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(), equalTo(after.metaData().index("test2"))); @@ -348,7 +348,7 @@ public void testAddWriteOnlyValidatesAgainstMetaDataBuilder() { ClusterState before = ClusterState.builder(ClusterName.DEFAULT) .metaData(MetaData.builder().put(indexMetaData).put(indexMetaData2)).build(); - Exception exception = expectThrows(IllegalStateException.class, () -> service.innerExecute(before, Arrays.asList( + Exception exception = expectThrows(IllegalStateException.class, () -> service.applyAliasActions(before, Arrays.asList( new AliasAction.Add("test", "alias", null, null, null, true), new AliasAction.Add("test2", "alias", null, null, null, true) ))); From ce974294754eeeffd6ca48d7589ad08722d2f399 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 20 Dec 2019 11:47:47 -0500 Subject: [PATCH 311/686] Close engine before reset log appender (#50390) Merge threads can run and access the mock appender after we have stopped it. Closes #50315 --- .../org/elasticsearch/index/engine/InternalEngineTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index c38f426dbfb59..79e5cf7489ca3 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -2369,7 +2369,7 @@ public void testIndexWriterInfoStream() throws IllegalAccessException, IOExcepti engine.index(indexForDoc(doc)); engine.flush(); assertTrue(mockAppender.sawIndexWriterMessage); - + engine.close(); } finally { Loggers.removeAppender(rootLogger, mockAppender); mockAppender.stop(); From daf0e9f2eef315c94157531794aaecf3afeb0b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Fri, 20 Dec 2019 18:01:05 +0100 Subject: [PATCH 312/686] Throw Error on deprecated nGram and edgeNGram custom filters (#50376) The camel-case `nGram` and `edgeNGram` filter names were deprecated in 6. We currently throw errors on new indices when they are used. However these errors are currently only thrown for pre-configured filters, adding them as custom filters doesn't trigger the warning and error. This change adds the appropriate exceptions for `nGram` and `edgeNGram` respectively. Closes #50360 --- .../analysis/common/CommonAnalysisPlugin.java | 40 +++++- .../common/CommonAnalysisPluginTests.java | 129 ++++++++++++++++++ 2 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java index f9d8cb28f6c61..b9de97952eb4b 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java @@ -118,9 +118,11 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AnalyzerProvider; import org.elasticsearch.index.analysis.CharFilterFactory; import org.elasticsearch.index.analysis.PreBuiltAnalyzerProviderFactory; @@ -238,7 +240,24 @@ public Map> getTokenFilters() { filters.put("dictionary_decompounder", requiresAnalysisSettings(DictionaryCompoundWordTokenFilterFactory::new)); filters.put("dutch_stem", DutchStemTokenFilterFactory::new); filters.put("edge_ngram", EdgeNGramTokenFilterFactory::new); - filters.put("edgeNGram", EdgeNGramTokenFilterFactory::new); + filters.put("edgeNGram", (IndexSettings indexSettings, Environment environment, String name, Settings settings) -> { + return new EdgeNGramTokenFilterFactory(indexSettings, environment, name, settings) { + @Override + public TokenStream create(TokenStream tokenStream) { + if (indexSettings.getIndexVersionCreated().onOrAfter(org.elasticsearch.Version.V_8_0_0)) { + throw new IllegalArgumentException( + "The [edgeNGram] token filter name was deprecated in 6.4 and cannot be used in new indices. " + + "Please change the filter name to [edge_ngram] instead."); + } else { + deprecationLogger.deprecatedAndMaybeLog("edgeNGram_deprecation", + "The [edgeNGram] token filter name is deprecated and will be removed in a future version. " + + "Please change the filter name to [edge_ngram] instead."); + } + return super.create(tokenStream); + } + + }; + }); filters.put("elision", requiresAnalysisSettings(ElisionTokenFilterFactory::new)); filters.put("fingerprint", FingerprintTokenFilterFactory::new); filters.put("flatten_graph", FlattenGraphTokenFilterFactory::new); @@ -258,7 +277,24 @@ public Map> getTokenFilters() { filters.put("min_hash", MinHashTokenFilterFactory::new); filters.put("multiplexer", MultiplexerTokenFilterFactory::new); filters.put("ngram", NGramTokenFilterFactory::new); - filters.put("nGram", NGramTokenFilterFactory::new); + filters.put("nGram", (IndexSettings indexSettings, Environment environment, String name, Settings settings) -> { + return new NGramTokenFilterFactory(indexSettings, environment, name, settings) { + @Override + public TokenStream create(TokenStream tokenStream) { + if (indexSettings.getIndexVersionCreated().onOrAfter(org.elasticsearch.Version.V_8_0_0)) { + throw new IllegalArgumentException( + "The [nGram] token filter name was deprecated in 6.4 and cannot be used in new indices. " + + "Please change the filter name to [ngram] instead."); + } else { + deprecationLogger.deprecatedAndMaybeLog("nGram_deprecation", + "The [nGram] token filter name is deprecated and will be removed in a future version. " + + "Please change the filter name to [ngram] instead."); + } + return super.create(tokenStream); + } + + }; + }); filters.put("pattern_capture", requiresAnalysisSettings(PatternCaptureGroupTokenFilterFactory::new)); filters.put("pattern_replace", requiresAnalysisSettings(PatternReplaceTokenFilterFactory::new)); filters.put("persian_normalization", PersianNormalizationFilterFactory::new); diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java new file mode 100644 index 0000000000000..90190e42f2f85 --- /dev/null +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java @@ -0,0 +1,129 @@ +/* + * 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. + */ + +package org.elasticsearch.analysis.common; + +import org.apache.lucene.analysis.MockTokenizer; +import org.apache.lucene.analysis.Tokenizer; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.index.analysis.TokenFilterFactory; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.IndexSettingsModule; +import org.elasticsearch.test.VersionUtils; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; + +public class CommonAnalysisPluginTests extends ESTestCase { + + /** + * Check that the deprecated "nGram" filter throws exception for indices created since 7.0.0 and + * logs a warning for earlier indices when the filter is used as a custom filter + */ + public void testNGramFilterInCustomAnalyzerDeprecationError() throws IOException { + final Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put(IndexMetaData.SETTING_VERSION_CREATED, + VersionUtils.randomVersionBetween(random(), Version.V_8_0_0, Version.CURRENT)) + .put("index.analysis.analyzer.custom_analyzer.type", "custom") + .put("index.analysis.analyzer.custom_analyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.custom_analyzer.filter", "my_ngram") + .put("index.analysis.filter.my_ngram.type", "nGram") + .build(); + + try (CommonAnalysisPlugin commonAnalysisPlugin = new CommonAnalysisPlugin()) { + Map tokenFilters = createTestAnalysis(IndexSettingsModule.newIndexSettings("index", settings), + settings, commonAnalysisPlugin).tokenFilter; + TokenFilterFactory tokenFilterFactory = tokenFilters.get("nGram"); + Tokenizer tokenizer = new MockTokenizer(); + tokenizer.setReader(new StringReader("foo bar")); + + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> tokenFilterFactory.create(tokenizer)); + assertEquals("The [nGram] token filter name was deprecated in 6.4 and cannot be used in new indices. " + + "Please change the filter name to [ngram] instead.", ex.getMessage()); + } + + final Settings settingsPre7 = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put(IndexMetaData.SETTING_VERSION_CREATED, VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_6_0)) + .put("index.analysis.analyzer.custom_analyzer.type", "custom") + .put("index.analysis.analyzer.custom_analyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.custom_analyzer.filter", "my_ngram").put("index.analysis.filter.my_ngram.type", "nGram") + .build(); + try (CommonAnalysisPlugin commonAnalysisPlugin = new CommonAnalysisPlugin()) { + Map tokenFilters = createTestAnalysis(IndexSettingsModule.newIndexSettings("index", settingsPre7), + settingsPre7, commonAnalysisPlugin).tokenFilter; + TokenFilterFactory tokenFilterFactory = tokenFilters.get("nGram"); + Tokenizer tokenizer = new MockTokenizer(); + tokenizer.setReader(new StringReader("foo bar")); + assertNotNull(tokenFilterFactory.create(tokenizer)); + assertWarnings("The [nGram] token filter name is deprecated and will be removed in a future version. " + + "Please change the filter name to [ngram] instead."); + } + } + + /** + * Check that the deprecated "edgeNGram" filter throws exception for indices created since 7.0.0 and + * logs a warning for earlier indices when the filter is used as a custom filter + */ + public void testEdgeNGramFilterInCustomAnalyzerDeprecationError() throws IOException { + final Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put(IndexMetaData.SETTING_VERSION_CREATED, + VersionUtils.randomVersionBetween(random(), Version.V_8_0_0, Version.CURRENT)) + .put("index.analysis.analyzer.custom_analyzer.type", "custom") + .put("index.analysis.analyzer.custom_analyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.custom_analyzer.filter", "my_ngram") + .put("index.analysis.filter.my_ngram.type", "edgeNGram") + .build(); + + try (CommonAnalysisPlugin commonAnalysisPlugin = new CommonAnalysisPlugin()) { + Map tokenFilters = createTestAnalysis(IndexSettingsModule.newIndexSettings("index", settings), + settings, commonAnalysisPlugin).tokenFilter; + TokenFilterFactory tokenFilterFactory = tokenFilters.get("edgeNGram"); + Tokenizer tokenizer = new MockTokenizer(); + tokenizer.setReader(new StringReader("foo bar")); + + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> tokenFilterFactory.create(tokenizer)); + assertEquals("The [edgeNGram] token filter name was deprecated in 6.4 and cannot be used in new indices. " + + "Please change the filter name to [edge_ngram] instead.", ex.getMessage()); + } + + final Settings settingsPre7 = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put(IndexMetaData.SETTING_VERSION_CREATED, + VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_6_0)) + .put("index.analysis.analyzer.custom_analyzer.type", "custom") + .put("index.analysis.analyzer.custom_analyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.custom_analyzer.filter", "my_ngram") + .put("index.analysis.filter.my_ngram.type", "edgeNGram") + .build(); + + try (CommonAnalysisPlugin commonAnalysisPlugin = new CommonAnalysisPlugin()) { + Map tokenFilters = createTestAnalysis(IndexSettingsModule.newIndexSettings("index", settingsPre7), + settingsPre7, commonAnalysisPlugin).tokenFilter; + TokenFilterFactory tokenFilterFactory = tokenFilters.get("edgeNGram"); + Tokenizer tokenizer = new MockTokenizer(); + tokenizer.setReader(new StringReader("foo bar")); + assertNotNull(tokenFilterFactory.create(tokenizer)); + assertWarnings("The [edgeNGram] token filter name is deprecated and will be removed in a future version. " + + "Please change the filter name to [edge_ngram] instead."); + } + } +} From 2e511cd143602e808a5fedc0eb0d92f98c133b09 Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Fri, 20 Dec 2019 11:41:18 -0700 Subject: [PATCH 313/686] Make ILMHistoryStore.putAsync truly async (#50403) * Make ILMHistoryStore.putAsync truly async This moves the `putAsync` method in `ILMHistoryStore` never to block. Previously due to the way that the `BulkProcessor` works, it was possible for `BulkProcessor#add` to block executing a bulk request. This was bad as we may be adding things to the history store in cluster state update threads. This also moves the index creation to be done prior to the bulk request execution, rather than being checked every time an operation was added to the queue. This lessens the chance of the index being created, then deleted (by some external force), and then recreated via a bulk indexing request. Resolves #50353 --- .../ilm/TimeSeriesLifecycleActionsIT.java | 2 - .../xpack/ilm/IndexLifecycle.java | 3 +- .../xpack/ilm/history/ILMHistoryStore.java | 77 ++++++++++++++----- .../xpack/ilm/IndexLifecycleRunnerTests.java | 2 +- .../ilm/history/ILMHistoryStoreTests.java | 4 +- .../slm/SLMSnapshotBlockingIntegTests.java | 8 ++ 6 files changed, 70 insertions(+), 26 deletions(-) diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index c40e96bcdd9e8..e872de5b08f4e 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -1012,7 +1012,6 @@ public void testILMRolloverOnManuallyRolledIndex() throws Exception { assertBusy(() -> assertTrue(indexExists(thirdIndex))); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50353") public void testHistoryIsWrittenWithSuccess() throws Exception { String index = "index"; @@ -1055,7 +1054,6 @@ public void testHistoryIsWrittenWithSuccess() throws Exception { assertBusy(() -> assertHistoryIsPresent(policy, index + "-000002", true, "check-rollover-ready"), 30, TimeUnit.SECONDS); } - @AwaitsFix( bugUrl = "https://github.com/elastic/elasticsearch/issues/50353") public void testHistoryIsWrittenWithFailure() throws Exception { String index = "index"; diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java index 4051b291b9801..3e1ad336905eb 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java @@ -179,7 +179,8 @@ public Collection createComponents(Client client, ClusterService cluster @SuppressWarnings("unused") ILMHistoryTemplateRegistry ilmTemplateRegistry = new ILMHistoryTemplateRegistry(settings, clusterService, threadPool, client, xContentRegistry); - ilmHistoryStore.set(new ILMHistoryStore(settings, new OriginSettingClient(client, INDEX_LIFECYCLE_ORIGIN), clusterService)); + ilmHistoryStore.set(new ILMHistoryStore(settings, new OriginSettingClient(client, INDEX_LIFECYCLE_ORIGIN), + clusterService, threadPool)); indexLifecycleInitialisationService.set(new IndexLifecycleService(settings, client, clusterService, threadPool, getClock(), System::currentTimeMillis, xContentRegistry, ilmHistoryStore.get())); components.add(indexLifecycleInitialisationService.get()); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java index ab8168de4b28d..96c54e5adfca3 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java @@ -9,6 +9,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ResourceAlreadyExistsException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.alias.Alias; @@ -31,11 +32,13 @@ import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.threadpool.ThreadPool; import java.io.Closeable; import java.io.IOException; import java.util.Arrays; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -54,26 +57,52 @@ public class ILMHistoryStore implements Closeable { public static final String ILM_HISTORY_INDEX_PREFIX = "ilm-history-" + INDEX_TEMPLATE_VERSION + "-"; public static final String ILM_HISTORY_ALIAS = "ilm-history-" + INDEX_TEMPLATE_VERSION; - private final Client client; - private final ClusterService clusterService; private final boolean ilmHistoryEnabled; private final BulkProcessor processor; + private final ThreadPool threadPool; - public ILMHistoryStore(Settings nodeSettings, Client client, ClusterService clusterService) { - this.client = client; - this.clusterService = clusterService; - ilmHistoryEnabled = LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING.get(nodeSettings); + public ILMHistoryStore(Settings nodeSettings, Client client, ClusterService clusterService, ThreadPool threadPool) { + this.ilmHistoryEnabled = LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING.get(nodeSettings); + this.threadPool = threadPool; this.processor = BulkProcessor.builder( new OriginSettingClient(client, INDEX_LIFECYCLE_ORIGIN)::bulk, new BulkProcessor.Listener() { @Override - public void beforeBulk(long executionId, BulkRequest request) { } + public void beforeBulk(long executionId, BulkRequest request) { + // Prior to actually performing the bulk, we should ensure the index exists, and + // if we were unable to create it or it was in a bad state, we should not + // attempt to index documents. + try { + final CompletableFuture indexCreated = new CompletableFuture<>(); + ensureHistoryIndex(client, clusterService.state(), ActionListener.wrap(indexCreated::complete, + ex -> { + logger.warn("failed to create ILM history store index prior to issuing bulk request", ex); + indexCreated.completeExceptionally(ex); + })); + indexCreated.get(2, TimeUnit.MINUTES); + } catch (Exception e) { + logger.warn(new ParameterizedMessage("unable to index the following ILM history items:\n{}", + request.requests().stream() + .filter(dwr -> (dwr instanceof IndexRequest)) + .map(dwr -> ((IndexRequest) dwr)) + .map(IndexRequest::sourceAsMap) + .map(Object::toString) + .collect(Collectors.joining("\n"))), e); + throw new ElasticsearchException(e); + } + } @Override public void afterBulk(long executionId, BulkRequest request, BulkResponse response) { long items = request.numberOfActions(); - logger.trace("indexed [{}] items into ILM history index", items); + if (logger.isTraceEnabled()) { + logger.trace("indexed [{}] items into ILM history index [{}]", items, + Arrays.stream(response.getItems()) + .map(BulkItemResponse::getIndex) + .distinct() + .collect(Collectors.joining(","))); + } if (response.hasFailures()) { Map failures = Arrays.stream(response.getItems()) .filter(BulkItemResponse::isFailed) @@ -105,18 +134,25 @@ public void putAsync(ILMHistoryItem item) { LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING.getKey(), item); return; } - logger.trace("about to index ILM history item in index [{}]: [{}]", ILM_HISTORY_ALIAS, item); - ensureHistoryIndex(client, clusterService.state(), ActionListener.wrap(createdIndex -> { - try (XContentBuilder builder = XContentFactory.jsonBuilder()) { - item.toXContent(builder, ToXContent.EMPTY_PARAMS); - IndexRequest request = new IndexRequest(ILM_HISTORY_ALIAS).source(builder); - processor.add(request); - } catch (IOException exception) { - logger.error(new ParameterizedMessage("failed to index ILM history item in index [{}]: [{}]", - ILM_HISTORY_ALIAS, item), exception); - } - }, ex -> logger.error(new ParameterizedMessage("failed to ensure ILM history index exists, not indexing history item [{}]", - item), ex))); + logger.trace("queueing ILM history item for indexing [{}]: [{}]", ILM_HISTORY_ALIAS, item); + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + item.toXContent(builder, ToXContent.EMPTY_PARAMS); + IndexRequest request = new IndexRequest(ILM_HISTORY_ALIAS).source(builder); + // TODO: remove the threadpool wrapping when the .add call is non-blocking + // (it can currently execute the bulk request occasionally) + // see: https://github.com/elastic/elasticsearch/issues/50440 + threadPool.executor(ThreadPool.Names.GENERIC).execute(() -> { + try { + processor.add(request); + } catch (Exception e) { + logger.error(new ParameterizedMessage("failed add ILM history item to queue for index [{}]: [{}]", + ILM_HISTORY_ALIAS, item), e); + } + }); + } catch (IOException exception) { + logger.error(new ParameterizedMessage("failed to queue ILM history item in index [{}]: [{}]", + ILM_HISTORY_ALIAS, item), exception); + } } /** @@ -134,6 +170,7 @@ static void ensureHistoryIndex(Client client, ClusterState state, ActionListener if (ilmHistory == null && initialHistoryIndex == null) { // No alias or index exists with the expected names, so create the index with appropriate alias + logger.debug("creating ILM history index [{}]", initialHistoryIndexName); client.admin().indices().prepareCreate(initialHistoryIndexName) .setWaitForActiveShards(1) .addAlias(new Alias(ILM_HISTORY_ALIAS) diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java index 130a77cf853dd..faa1270f479aa 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java @@ -1118,7 +1118,7 @@ private class NoOpHistoryStore extends ILMHistoryStore { private final List items = new ArrayList<>(); NoOpHistoryStore() { - super(Settings.EMPTY, noopClient, null); + super(Settings.EMPTY, noopClient, null, null); } public List getItems() { diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStoreTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStoreTests.java index 3e6b9a638737c..47a1ba406e502 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStoreTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStoreTests.java @@ -68,7 +68,7 @@ public void setup() { threadPool = new TestThreadPool(this.getClass().getName()); client = new VerifyingClient(threadPool); clusterService = ClusterServiceUtils.createClusterService(threadPool); - historyStore = new ILMHistoryStore(Settings.EMPTY, client, clusterService); + historyStore = new ILMHistoryStore(Settings.EMPTY, client, clusterService, threadPool); } @After @@ -81,7 +81,7 @@ public void setdown() { public void testNoActionIfDisabled() throws Exception { Settings settings = Settings.builder().put(LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING.getKey(), false).build(); - try (ILMHistoryStore disabledHistoryStore = new ILMHistoryStore(settings, client, null)) { + try (ILMHistoryStore disabledHistoryStore = new ILMHistoryStore(settings, client, null, threadPool)) { String policyId = randomAlphaOfLength(5); final long timestamp = randomNonNegativeLong(); ILMHistoryItem record = ILMHistoryItem.success("index", policyId, timestamp, null, null); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/slm/SLMSnapshotBlockingIntegTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/slm/SLMSnapshotBlockingIntegTests.java index 7c7a555a8d5d9..16f388fa49481 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/slm/SLMSnapshotBlockingIntegTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/slm/SLMSnapshotBlockingIntegTests.java @@ -29,6 +29,7 @@ import org.elasticsearch.snapshots.mockstore.MockRepository; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; +import org.elasticsearch.xpack.core.ilm.LifecycleSettings; import org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicy; import org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicyItem; import org.elasticsearch.xpack.core.slm.SnapshotRetentionConfiguration; @@ -64,6 +65,13 @@ public class SLMSnapshotBlockingIntegTests extends ESIntegTestCase { static final String REPO = "my-repo"; List dataNodeNames = null; + @Override + protected Settings nodeSettings(int nodeOrdinal) { + return Settings.builder().put(super.nodeSettings(nodeOrdinal)) + .put(LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED, false) + .build(); + } + @Before public void ensureClusterNodes() { logger.info("--> starting enough nodes to ensure we have enough to safely stop for tests"); From b577713e6ad921ccfa457fa0aa56158599ec1517 Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Fri, 20 Dec 2019 10:52:44 -0800 Subject: [PATCH 314/686] Document use of context in put stored script (#50446) This documents how to test compile a stored script against a specific context when using PUT/POST. --- docs/reference/scripting/using.asciidoc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/reference/scripting/using.asciidoc b/docs/reference/scripting/using.asciidoc index 02a3cc6042c05..9d056415d8f76 100644 --- a/docs/reference/scripting/using.asciidoc +++ b/docs/reference/scripting/using.asciidoc @@ -155,6 +155,22 @@ POST _scripts/calculate-score ----------------------------------- // TEST[setup:twitter] +You may also specify a context as part of the url path to compile a +stored script against that specific context in the form of +`/_scripts/{id}/{context}`: + +[source,console] +----------------------------------- +POST _scripts/calculate-score/score +{ + "script": { + "lang": "painless", + "source": "Math.log(_score * 2) + params.my_modifier" + } +} +----------------------------------- +// TEST[setup:twitter] + This same script can be retrieved with: [source,console] From c18fea389357ba005b11fb4458d386ffd8127309 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Fri, 20 Dec 2019 14:02:42 -0500 Subject: [PATCH 315/686] [DOCS] Remove outdated file scripts refererence (#50437) File scripts were removed in 6.0 with #24627. This removes an outdated file scripts reference from the conditional clauses section of the search templates docs. --- docs/reference/search/search-template.asciidoc | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/docs/reference/search/search-template.asciidoc b/docs/reference/search/search-template.asciidoc index aa73e9d337232..0183213cd1fcc 100644 --- a/docs/reference/search/search-template.asciidoc +++ b/docs/reference/search/search-template.asciidoc @@ -545,8 +545,8 @@ The `params` would look like: // NOTCONSOLE <1> The `line_no`, `start`, and `end` parameters are optional. - -We could write the query as: +When written as a query, the template would include invalid JSON, such as +section markers like `{{#line_no}}`: [source,js] ------------------------------------------ @@ -587,19 +587,14 @@ We could write the query as: <6> Include the `lte` clause only if `line_no.end` is specified <7> Fill in the value of param `line_no.end` -[NOTE] -================================== -As written above, this template is not valid JSON because it includes the -_section_ markers like `{{#line_no}}`. For this reason, the template should -either be stored in a file (see <>) or, when used -via the REST API, should be written as a string: +Because search templates cannot include invalid JSON, you can pass the same +query as a string instead: [source,js] -------------------- "source": "{\"query\":{\"bool\":{\"must\":{\"match\":{\"line\":\"{{text}}\"}},\"filter\":{{{#line_no}}\"range\":{\"line_no\":{{{#start}}\"gte\":\"{{start}}\"{{#end}},{{/end}}{{/start}}{{#end}}\"lte\":\"{{end}}\"{{/end}}}}{{/line_no}}}}}}" -------------------- // NOTCONSOLE -================================== [[search-template-encode-urls]] From 9f239d6d740005c6673baa485fd6c1cd5007204d Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 20 Dec 2019 14:32:37 -0500 Subject: [PATCH 316/686] [ML][Inference] minor cleanup for inference (#50444) --- .../core/ml/inference/InferenceToXContentCompressor.java | 4 ++-- .../xpack/ml/inference/ingest/InferenceProcessor.java | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/InferenceToXContentCompressor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/InferenceToXContentCompressor.java index f2d24c81b8695..96b4eb9d6493a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/InferenceToXContentCompressor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/InferenceToXContentCompressor.java @@ -43,8 +43,8 @@ public static String deflate(T objectToCompress) th } static T inflate(String compressedString, - CheckedFunction parserFunction, - NamedXContentRegistry xContentRegistry) throws IOException { + CheckedFunction parserFunction, + NamedXContentRegistry xContentRegistry) throws IOException { try(XContentParser parser = XContentHelper.createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, inflate(compressedString, MAX_INFLATED_BYTES), diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java index 18ac57d1ae818..cc2e7997d6318 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ingest/InferenceProcessor.java @@ -28,7 +28,6 @@ import org.elasticsearch.ingest.Processor; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.ml.action.InternalInferModelAction; -import org.elasticsearch.xpack.core.ml.inference.results.InferenceResults; import org.elasticsearch.xpack.core.ml.inference.results.WarningInferenceResults; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ClassificationConfig; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.InferenceConfig; @@ -151,12 +150,8 @@ void mutateDocument(InternalInferModelAction.Response response, IngestDocument i if (response.getInferenceResults().isEmpty()) { throw new ElasticsearchStatusException("Unexpected empty inference response", RestStatus.INTERNAL_SERVER_ERROR); } - InferenceResults inferenceResults = response.getInferenceResults().get(0); - if (inferenceResults instanceof WarningInferenceResults) { - inferenceResults.writeResult(ingestDocument, this.targetField); - } else { - response.getInferenceResults().get(0).writeResult(ingestDocument, this.targetField); - } + assert response.getInferenceResults().size() == 1; + response.getInferenceResults().get(0).writeResult(ingestDocument, this.targetField); ingestDocument.setFieldValue(targetField + "." + MODEL_ID, modelId); } From 49d711f649b58810b179eb609507032d0a28c824 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Fri, 20 Dec 2019 15:01:19 -0800 Subject: [PATCH 317/686] Upgrade to Gradle Enterprise plugin 3.1.1 (#50451) --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index d92f5584a27a9..0d3c40a00ff7f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ plugins { - id "com.gradle.enterprise" version "3.0" + id "com.gradle.enterprise" version "3.1.1" } String dirName = rootProject.projectDir.name From 87f18ffb98616ebc79d6618c000d4988e6c6f5da Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Mon, 23 Dec 2019 10:48:58 +0100 Subject: [PATCH 318/686] Unmute 'Test url escaping with url mustache function' webhook watcher test (#50439) Some changes had to be made in order to make the test pass due to the removal or types. Added some more assertions. The failure description in this comment [0] indicates that the rest handler couldn't be found. The test passes now. I plan to merge this into master and see how CI reacts, if it handles this change well then I will also unmute this test in 7 dot x branch. Relates to #41172 0: https://github.com/elastic/elasticsearch/issues/41172#issuecomment-496993976 --- .../test/mustache/50_webhook_url_escaping.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/50_webhook_url_escaping.yml b/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/50_webhook_url_escaping.yml index bff41eceab3f7..88986a5546c7c 100644 --- a/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/50_webhook_url_escaping.yml +++ b/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/50_webhook_url_escaping.yml @@ -1,8 +1,5 @@ --- "Test url escaping with url mustache function": - - skip: - version: "all" - reason: "AwaitsFix https://github.com/elastic/elasticsearch/issues/41172" - do: cluster.health: wait_for_status: yellow @@ -10,7 +7,6 @@ - do: index: index: - type: log id: 1 refresh: true body: { foo: bar } @@ -36,10 +32,15 @@ pipeline: description: _description processors: [ grok: { field: host, patterns : ["%{IPORHOST:hostname}:%{NUMBER:port:int}"] } ] - docs: [ { _index: index, _type: type, _id: id, _source: { host: $host } } ] + docs: [ { _index: index, _id: id, _source: { host: $host } } ] - set: { docs.0.doc._source.hostname: hostname } - set: { docs.0.doc._source.port: port } + - do: + count: + index: + - match: {count: 1} + - do: watcher.put_watch: id: "test_watch" @@ -67,7 +68,7 @@ method: PUT host: $hostname port: $port - path: "/{{#url}}{{ctx.metadata.index}}{{/url}}/log/2" + path: "/{{#url}}{{ctx.metadata.index}}{{/url}}/_doc/2" params: refresh: "true" body: "{ \"foo\": \"bar\" }" @@ -78,6 +79,8 @@ - do: watcher.execute_watch: id: "test_watch" + - match: {watch_record.result.condition.met: true} + - match: {watch_record.result.actions.0.status: 'success'} - do: count: From df24374f2248640049db2a4408d593154d198ad8 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 23 Dec 2019 11:23:39 +0100 Subject: [PATCH 319/686] Fix Source Only Snapshot REST Test Failure (#50456) We are matching on the exact number of shards in this test, but may run into snapshotting more than the single index created in it due to auto-created indices like `.watcher`. Fixed by making the test only take a snapshot of the single index used by this test. Closes #50450 --- .../src/test/resources/rest-api-spec/test/snapshot/10_basic.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/snapshot/10_basic.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/snapshot/10_basic.yml index 58ace059d04c1..e0ac436718595 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/snapshot/10_basic.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/snapshot/10_basic.yml @@ -39,6 +39,8 @@ setup: repository: test_repo_restore_1 snapshot: test_snapshot wait_for_completion: true + body: | + { "indices": "test_index" } - match: { snapshot.snapshot: test_snapshot } - match: { snapshot.state : SUCCESS } From 19a922fee5bf9222269272e3dfd81178c72a647c Mon Sep 17 00:00:00 2001 From: Xiang Dai <764524258@qq.com> Date: Mon, 23 Dec 2019 23:35:14 +0800 Subject: [PATCH 320/686] Fix docs typos (#50365) Fixes a few typos in the docs. Signed-off-by: Xiang Dai 764524258@qq.com --- docs/painless/painless-guide/painless-datetime.asciidoc | 8 ++++---- docs/plugins/discovery-ec2.asciidoc | 2 +- docs/plugins/mapper-annotated-text.asciidoc | 2 +- docs/reference/analysis/analyzers/lang-analyzer.asciidoc | 4 ++-- docs/reference/docs/delete-by-query.asciidoc | 2 +- docs/reference/docs/get.asciidoc | 2 +- docs/reference/docs/update-by-query.asciidoc | 2 +- docs/reference/ilm/apis/slm-api.asciidoc | 2 +- docs/reference/ilm/getting-started-slm.asciidoc | 4 ++-- docs/reference/mapping/types/shape.asciidoc | 2 +- docs/reference/query-dsl/exists-query.asciidoc | 2 +- docs/reference/search/validate.asciidoc | 2 +- docs/reference/setup/install/deb.asciidoc | 2 +- docs/reference/setup/install/rpm.asciidoc | 2 +- docs/reference/sql/language/syntax/lexic/index.asciidoc | 4 ++-- 15 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/painless/painless-guide/painless-datetime.asciidoc b/docs/painless/painless-guide/painless-datetime.asciidoc index 68d93f4e3693d..17aed8fb90904 100644 --- a/docs/painless/painless-guide/painless-datetime.asciidoc +++ b/docs/painless/painless-guide/painless-datetime.asciidoc @@ -293,7 +293,7 @@ if (timestamp1 > timestamp2) { } ---- + -* Equality comparision of two complex datetimes +* Equality comparison of two complex datetimes + [source,Painless] ---- @@ -307,7 +307,7 @@ if (zdt1.equals(zdt2)) { } ---- + -* Less than comparision of two complex datetimes +* Less than comparison of two complex datetimes + [source,Painless] ---- @@ -321,7 +321,7 @@ if (zdt1.isBefore(zdt2)) { } ---- + -* Greater than comparision of two complex datetimes +* Greater than comparison of two complex datetimes + [source,Painless] ---- @@ -611,7 +611,7 @@ per document, so each time the script is run a different `now` is returned. The second is scripts are often run in a distributed fashion without a way to appropriately synchronize `now`. Instead, pass in a user-defined parameter with either a string datetime or numeric datetime for `now`. A numeric datetime is -preferred as there is no need to parse it for comparision. +preferred as there is no need to parse it for comparison. ===== Datetime Now Examples diff --git a/docs/plugins/discovery-ec2.asciidoc b/docs/plugins/discovery-ec2.asciidoc index c1a4913170301..4ea3ab826bd70 100644 --- a/docs/plugins/discovery-ec2.asciidoc +++ b/docs/plugins/discovery-ec2.asciidoc @@ -290,7 +290,7 @@ available on AWS-based infrastructure from http://www.elastic.co/cloud. ===== Storage EC2 instances offer a number of different kinds of storage. Please be aware of -the folowing when selecting the storage for your cluster: +the following when selecting the storage for your cluster: * http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html[Instance Store] is recommended for {es} clusters as it offers excellent performance and diff --git a/docs/plugins/mapper-annotated-text.asciidoc b/docs/plugins/mapper-annotated-text.asciidoc index 151fccfd73344..ff1a497fb6c9a 100644 --- a/docs/plugins/mapper-annotated-text.asciidoc +++ b/docs/plugins/mapper-annotated-text.asciidoc @@ -137,7 +137,7 @@ GET my_index/_search inject the single token value `Beck` at the same position as `beck` in the token stream. <2> Note annotations can inject multiple tokens at the same position - here we inject both the very specific value `Jeff Beck` and the broader term `Guitarist`. This enables -broader positional queries e.g. finding mentions of a `Guitarist` near to `strat`. +broader positional queries e.g. finding mentions of a `Guitarist` near to `start`. <3> A benefit of searching with these carefully defined annotation tokens is that a query for `Beck` will not match document 2 that contains the tokens `jeff`, `beck` and `Jeff Beck` diff --git a/docs/reference/analysis/analyzers/lang-analyzer.asciidoc b/docs/reference/analysis/analyzers/lang-analyzer.asciidoc index a57fef8d28e96..dbcbc17e44e96 100644 --- a/docs/reference/analysis/analyzers/lang-analyzer.asciidoc +++ b/docs/reference/analysis/analyzers/lang-analyzer.asciidoc @@ -388,7 +388,7 @@ PUT /catalan_example }, "catalan_keywords": { "type": "keyword_marker", - "keywords": ["exemple"] <2> + "keywords": ["example"] <2> }, "catalan_stemmer": { "type": "stemmer", @@ -798,7 +798,7 @@ PUT /french_example }, "french_keywords": { "type": "keyword_marker", - "keywords": ["Exemple"] <2> + "keywords": ["Example"] <2> }, "french_stemmer": { "type": "stemmer", diff --git a/docs/reference/docs/delete-by-query.asciidoc b/docs/reference/docs/delete-by-query.asciidoc index 71d00528e80cc..093504f712adb 100644 --- a/docs/reference/docs/delete-by-query.asciidoc +++ b/docs/reference/docs/delete-by-query.asciidoc @@ -74,7 +74,7 @@ and all failed requests are returned in the response. Any delete requests that completed successfully still stick, they are not rolled back. You can opt to count version conflicts instead of halting and returning by -setting `conflicts` to `proceeed`. +setting `conflicts` to `proceed`. ===== Refreshing shards diff --git a/docs/reference/docs/get.asciidoc b/docs/reference/docs/get.asciidoc index 525eba79a03d3..a26b7cdf061f9 100644 --- a/docs/reference/docs/get.asciidoc +++ b/docs/reference/docs/get.asciidoc @@ -206,7 +206,7 @@ The explicit routing, if set. '_source':: If `found` is `true`, contains the document data formatted in JSON. Excluded if the `_source` parameter is set to `false` or the `stored_fields` -paramter is set to `true`. +parameter is set to `true`. '_fields':: If the `stored_fields` parameter is set to `true` and `found` is diff --git a/docs/reference/docs/update-by-query.asciidoc b/docs/reference/docs/update-by-query.asciidoc index 2f70994244b54..83d1105deaadf 100644 --- a/docs/reference/docs/update-by-query.asciidoc +++ b/docs/reference/docs/update-by-query.asciidoc @@ -59,7 +59,7 @@ When the versions match, the document is updated and the version number is incre If a document changes between the time that the snapshot is taken and the update operation is processed, it results in a version conflict and the operation fails. You can opt to count version conflicts instead of halting and returning by -setting `conflicts` to `proceeed`. +setting `conflicts` to `proceed`. NOTE: Documents with a version equal to 0 cannot be updated using update by query because `internal` versioning does not support 0 as a valid diff --git a/docs/reference/ilm/apis/slm-api.asciidoc b/docs/reference/ilm/apis/slm-api.asciidoc index 890e645a989d4..51e095f72ef69 100644 --- a/docs/reference/ilm/apis/slm-api.asciidoc +++ b/docs/reference/ilm/apis/slm-api.asciidoc @@ -300,7 +300,7 @@ The API returns the following response: } -------------------------------------------------- // TESTRESPONSE[s/"modified_date": "2019-04-23T01:30:00.000Z"/"modified_date": $body.daily-snapshots.modified_date/ s/"modified_date_millis": 1556048137314/"modified_date_millis": $body.daily-snapshots.modified_date_millis/ s/"next_execution": "2019-04-24T01:30:00.000Z"/"next_execution": $body.daily-snapshots.next_execution/ s/"next_execution_millis": 1556048160000/"next_execution_millis": $body.daily-snapshots.next_execution_millis/] -<1> The version of the snapshot policy, only the latest verison is stored and incremented when the policy is updated +<1> The version of the snapshot policy, only the latest version is stored and incremented when the policy is updated <2> The last time this policy was modified. <3> The next time this policy will be executed. diff --git a/docs/reference/ilm/getting-started-slm.asciidoc b/docs/reference/ilm/getting-started-slm.asciidoc index 54ebef9a8dd3b..f09a631e8abe1 100644 --- a/docs/reference/ilm/getting-started-slm.asciidoc +++ b/docs/reference/ilm/getting-started-slm.asciidoc @@ -137,7 +137,7 @@ While snapshots taken by SLM policies can be viewed through the standard snapsho API, SLM also keeps track of policy successes and failures in ways that are a bit easier to use to make sure the policy is working. Once a policy has executed at least once, when you view the policy using the <>, -some metadata will be returned indicating whether the snapshot was sucessfully +some metadata will be returned indicating whether the snapshot was successfully initiated or not. Instead of waiting for our policy to run, let's tell SLM to take a snapshot @@ -219,7 +219,7 @@ field - it's included above only as an example. You should, however, see a `last_success` field and a snapshot name. If you do, you've successfully taken your first snapshot using SLM! -While only the most recent sucess and failure are available through the Get Policy +While only the most recent success and failure are available through the Get Policy API, all policy executions are recorded to a history index, which may be queried by searching the index pattern `.slm-history*`. diff --git a/docs/reference/mapping/types/shape.asciidoc b/docs/reference/mapping/types/shape.asciidoc index 3a180690ff44d..9969021c7853d 100644 --- a/docs/reference/mapping/types/shape.asciidoc +++ b/docs/reference/mapping/types/shape.asciidoc @@ -61,7 +61,7 @@ and reject the whole document. Like `geo_shape`, the `shape` field type is indexed by decomposing geometries into a triangular mesh and indexing each triangle as a 7 dimension point in a BKD tree. The coordinates provided to the indexer are single precision floating point values so -the field guarantess the same accuracy provided by the java virtual machine (typically +the field guarantees the same accuracy provided by the java virtual machine (typically `1E-38`). For polygons/multi-polygons the performance of the tessellator primarily depends on the number of vertices that define the geometry. diff --git a/docs/reference/query-dsl/exists-query.asciidoc b/docs/reference/query-dsl/exists-query.asciidoc index 539e7208e0063..fd0f13b50ff50 100644 --- a/docs/reference/query-dsl/exists-query.asciidoc +++ b/docs/reference/query-dsl/exists-query.asciidoc @@ -33,7 +33,7 @@ GET /_search `field`:: (Required, string) Name of the field you wish to search. + -While a field is deemed non-existant if the JSON value is `null` or `[]`, these +While a field is deemed non-existent if the JSON value is `null` or `[]`, these values will indicate the field does exist: + * Empty strings, such as `""` or `"-"` diff --git a/docs/reference/search/validate.asciidoc b/docs/reference/search/validate.asciidoc index 9ec81279f3df6..c88ca44a4a3b9 100644 --- a/docs/reference/search/validate.asciidoc +++ b/docs/reference/search/validate.asciidoc @@ -53,7 +53,7 @@ include::{docdir}/rest-api/common-parms.asciidoc[tag=expand-wildcards] `explain`:: (Optional, boolean) If `true`, the response returns detailed information if an - error has occured. Defautls to `false`. + error has occurred. Defaults to `false`. include::{docdir}/rest-api/common-parms.asciidoc[tag=index-ignore-unavailable] diff --git a/docs/reference/setup/install/deb.asciidoc b/docs/reference/setup/install/deb.asciidoc index 30ce4a5c0264b..77853e2337b6e 100644 --- a/docs/reference/setup/install/deb.asciidoc +++ b/docs/reference/setup/install/deb.asciidoc @@ -240,7 +240,7 @@ locations for a Debian-based system: | jdk | The bundled Java Development Kit used to run Elasticsearch. Can - be overriden by setting the `JAVA_HOME` environment variable + be overridden by setting the `JAVA_HOME` environment variable in `/etc/default/elasticsearch`. | /usr/share/elasticsearch/jdk d| diff --git a/docs/reference/setup/install/rpm.asciidoc b/docs/reference/setup/install/rpm.asciidoc index f1601f95b1773..301bbdc43b47a 100644 --- a/docs/reference/setup/install/rpm.asciidoc +++ b/docs/reference/setup/install/rpm.asciidoc @@ -233,7 +233,7 @@ locations for an RPM-based system: | jdk | The bundled Java Development Kit used to run Elasticsearch. Can - be overriden by setting the `JAVA_HOME` environment variable + be overridden by setting the `JAVA_HOME` environment variable in `/etc/sysconfig/elasticsearch`. | /usr/share/elasticsearch/jdk d| diff --git a/docs/reference/sql/language/syntax/lexic/index.asciidoc b/docs/reference/sql/language/syntax/lexic/index.asciidoc index 9b2f78c35cd90..b1405f0c9cb70 100644 --- a/docs/reference/sql/language/syntax/lexic/index.asciidoc +++ b/docs/reference/sql/language/syntax/lexic/index.asciidoc @@ -148,7 +148,7 @@ s|Description Most operators in {es-sql} have the same precedence and are left-associative. As this is done at parsing time, parenthesis need to be used to enforce a different precedence. -The following table indicates the supported operators and their precendence (highest to lowest); +The following table indicates the supported operators and their precedence (highest to lowest); [cols="^2m,^,^3"] @@ -176,7 +176,7 @@ s|Description |+ - |left -|addition, substraction +|addition, subtraction |BETWEEN IN LIKE | From 95d57063d795d8be9d185221ecee69c4b729de24 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Mon, 23 Dec 2019 10:07:03 -0800 Subject: [PATCH 321/686] Centralize BoundingBox logic to a dedicated class (#50253) Both geo_bounding_box query and geo_bounds aggregation have a very similar definition of a "bounding box". A lot of this logic (serialization, xcontent-parsing, etc) can be centralized instead of having separated efforts to do the same things --- .../common/geo/GeoBoundingBox.java | 229 ++++++++++++++++++ .../query/GeoBoundingBoxQueryBuilder.java | 125 ++-------- .../metrics/InternalGeoBounds.java | 84 ++----- .../aggregations/metrics/ParsedGeoBounds.java | 34 +-- .../common/geo/GeoBoundingBoxTests.java | 149 ++++++++++++ ...gBoxIT.java => GeoBoundingBoxQueryIT.java} | 2 +- 6 files changed, 434 insertions(+), 189 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/common/geo/GeoBoundingBox.java create mode 100644 server/src/test/java/org/elasticsearch/common/geo/GeoBoundingBoxTests.java rename server/src/test/java/org/elasticsearch/search/geo/{GeoBoundingBoxIT.java => GeoBoundingBoxQueryIT.java} (99%) diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeoBoundingBox.java b/server/src/main/java/org/elasticsearch/common/geo/GeoBoundingBox.java new file mode 100644 index 0000000000000..78e81925275d7 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/GeoBoundingBox.java @@ -0,0 +1,229 @@ +/* + * 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. + */ +package org.elasticsearch.common.geo; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.ShapeType; +import org.elasticsearch.geometry.utils.StandardValidator; +import org.elasticsearch.geometry.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; +import java.util.Objects; + +/** + * A class representing a Geo-Bounding-Box for use by Geo queries and aggregations + * that deal with extents/rectangles representing rectangular areas of interest. + */ +public class GeoBoundingBox implements ToXContentObject, Writeable { + private static final WellKnownText WKT_PARSER = new WellKnownText(true, new StandardValidator(true)); + static final ParseField TOP_RIGHT_FIELD = new ParseField("top_right"); + static final ParseField BOTTOM_LEFT_FIELD = new ParseField("bottom_left"); + static final ParseField TOP_FIELD = new ParseField("top"); + static final ParseField BOTTOM_FIELD = new ParseField("bottom"); + static final ParseField LEFT_FIELD = new ParseField("left"); + static final ParseField RIGHT_FIELD = new ParseField("right"); + static final ParseField WKT_FIELD = new ParseField("wkt"); + public static final ParseField BOUNDS_FIELD = new ParseField("bounds"); + public static final ParseField LAT_FIELD = new ParseField("lat"); + public static final ParseField LON_FIELD = new ParseField("lon"); + public static final ParseField TOP_LEFT_FIELD = new ParseField("top_left"); + public static final ParseField BOTTOM_RIGHT_FIELD = new ParseField("bottom_right"); + + private final GeoPoint topLeft; + private final GeoPoint bottomRight; + + public GeoBoundingBox(GeoPoint topLeft, GeoPoint bottomRight) { + this.topLeft = topLeft; + this.bottomRight = bottomRight; + } + + public GeoBoundingBox(StreamInput input) throws IOException { + this.topLeft = input.readGeoPoint(); + this.bottomRight = input.readGeoPoint(); + } + + public GeoPoint topLeft() { + return topLeft; + } + + public GeoPoint bottomRight() { + return bottomRight; + } + + public double top() { + return topLeft.lat(); + } + + public double bottom() { + return bottomRight.lat(); + } + + public double left() { + return topLeft.lon(); + } + + public double right() { + return bottomRight.lon(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(BOUNDS_FIELD.getPreferredName()); + toXContentFragment(builder, true); + builder.endObject(); + return builder; + } + + public XContentBuilder toXContentFragment(XContentBuilder builder, boolean buildLatLonFields) throws IOException { + if (buildLatLonFields) { + builder.startObject(TOP_LEFT_FIELD.getPreferredName()); + builder.field(LAT_FIELD.getPreferredName(), topLeft.lat()); + builder.field(LON_FIELD.getPreferredName(), topLeft.lon()); + builder.endObject(); + } else { + builder.array(TOP_LEFT_FIELD.getPreferredName(), topLeft.lon(), topLeft.lat()); + } + if (buildLatLonFields) { + builder.startObject(BOTTOM_RIGHT_FIELD.getPreferredName()); + builder.field(LAT_FIELD.getPreferredName(), bottomRight.lat()); + builder.field(LON_FIELD.getPreferredName(), bottomRight.lon()); + builder.endObject(); + } else { + builder.array(BOTTOM_RIGHT_FIELD.getPreferredName(), bottomRight.lon(), bottomRight.lat()); + } + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeGeoPoint(topLeft); + out.writeGeoPoint(bottomRight); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GeoBoundingBox that = (GeoBoundingBox) o; + return topLeft.equals(that.topLeft) && + bottomRight.equals(that.bottomRight); + } + + @Override + public int hashCode() { + return Objects.hash(topLeft, bottomRight); + } + + @Override + public String toString() { + return "BBOX (" + topLeft.lon() + ", " + bottomRight.lon() + ", " + topLeft.lat() + ", " + bottomRight.lat() + ")"; + } + + /** + * Parses the bounding box and returns bottom, top, left, right coordinates + */ + public static GeoBoundingBox parseBoundingBox(XContentParser parser) throws IOException, ElasticsearchParseException { + XContentParser.Token token = parser.currentToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("failed to parse bounding box. Expected start object but found [{}]", token); + } + + double top = Double.NaN; + double bottom = Double.NaN; + double left = Double.NaN; + double right = Double.NaN; + + String currentFieldName; + GeoPoint sparse = new GeoPoint(); + Rectangle envelope = null; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + token = parser.nextToken(); + if (WKT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + try { + Geometry geometry = WKT_PARSER.fromWKT(parser.text()); + if (ShapeType.ENVELOPE.equals(geometry.type()) == false) { + throw new ElasticsearchParseException("failed to parse WKT bounding box. [" + + geometry.type() + "] found. expected [" + ShapeType.ENVELOPE + "]"); + } + envelope = (Rectangle) geometry; + } catch (ParseException|IllegalArgumentException e) { + throw new ElasticsearchParseException("failed to parse WKT bounding box", e); + } + } else if (TOP_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + top = parser.doubleValue(); + } else if (BOTTOM_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + bottom = parser.doubleValue(); + } else if (LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + left = parser.doubleValue(); + } else if (RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + right = parser.doubleValue(); + } else { + if (TOP_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_LEFT); + top = sparse.getLat(); + left = sparse.getLon(); + } else if (BOTTOM_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_RIGHT); + bottom = sparse.getLat(); + right = sparse.getLon(); + } else if (TOP_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_RIGHT); + top = sparse.getLat(); + right = sparse.getLon(); + } else if (BOTTOM_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_LEFT); + bottom = sparse.getLat(); + left = sparse.getLon(); + } else { + throw new ElasticsearchParseException("failed to parse bounding box. unexpected field [{}]", currentFieldName); + } + } + } else { + throw new ElasticsearchParseException("failed to parse bounding box. field name expected but [{}] found", token); + } + } + if (envelope != null) { + if (Double.isNaN(top) == false || Double.isNaN(bottom) == false || Double.isNaN(left) == false || + Double.isNaN(right) == false) { + throw new ElasticsearchParseException("failed to parse bounding box. Conflicting definition found " + + "using well-known text and explicit corners."); + } + GeoPoint topLeft = new GeoPoint(envelope.getMaxLat(), envelope.getMinLon()); + GeoPoint bottomRight = new GeoPoint(envelope.getMinLat(), envelope.getMaxLon()); + return new GeoBoundingBox(topLeft, bottomRight); + } + GeoPoint topLeft = new GeoPoint(top, left); + GeoPoint bottomRight = new GeoPoint(bottom, right); + return new GeoBoundingBox(topLeft, bottomRight); + } + +} diff --git a/server/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilder.java index 699d4b79aa72b..59025dc03e161 100644 --- a/server/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilder.java @@ -21,7 +21,6 @@ import org.apache.lucene.document.LatLonDocValuesField; import org.apache.lucene.document.LatLonPoint; -//import org.apache.lucene.geo.Rectangle; import org.apache.lucene.search.IndexOrDocValuesQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; @@ -29,11 +28,9 @@ import org.elasticsearch.common.Numbers; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.GeoPoint; -import org.elasticsearch.common.geo.GeoShapeType; import org.elasticsearch.common.geo.GeoUtils; -import org.elasticsearch.common.geo.builders.EnvelopeBuilder; -import org.elasticsearch.common.geo.parsers.GeoWKTParser; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -66,24 +63,12 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder path) { if (path.isEmpty()) { return this; } else if (path.size() == 1) { - BoundingBox boundingBox = resolveBoundingBox(); + GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox(); String bBoxSide = path.get(0); switch (bBoxSide) { case "top": - return boundingBox.topLeft.lat(); + return geoBoundingBox.top(); case "left": - return boundingBox.topLeft.lon(); + return geoBoundingBox.left(); case "bottom": - return boundingBox.bottomRight.lat(); + return geoBoundingBox.bottom(); case "right": - return boundingBox.bottomRight.lon(); + return geoBoundingBox.right(); default: throw new IllegalArgumentException("Found unknown path element [" + bBoxSide + "] in [" + getName() + "]"); } } else if (path.size() == 2) { - BoundingBox boundingBox = resolveBoundingBox(); + GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox(); GeoPoint cornerPoint = null; String cornerString = path.get(0); switch (cornerString) { case "top_left": - cornerPoint = boundingBox.topLeft; + cornerPoint = geoBoundingBox.topLeft(); break; case "bottom_right": - cornerPoint = boundingBox.bottomRight; + cornerPoint = geoBoundingBox.bottomRight(); break; default: throw new IllegalArgumentException("Found unknown path element [" + cornerString + "] in [" + getName() + "]"); @@ -176,78 +168,50 @@ public Object getProperty(List path) { @Override public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { - GeoPoint topLeft = topLeft(); - GeoPoint bottomRight = bottomRight(); - if (topLeft != null) { - builder.startObject(BOUNDS_FIELD.getPreferredName()); - builder.startObject(TOP_LEFT_FIELD.getPreferredName()); - builder.field(LAT_FIELD.getPreferredName(), topLeft.lat()); - builder.field(LON_FIELD.getPreferredName(), topLeft.lon()); - builder.endObject(); - builder.startObject(BOTTOM_RIGHT_FIELD.getPreferredName()); - builder.field(LAT_FIELD.getPreferredName(), bottomRight.lat()); - builder.field(LON_FIELD.getPreferredName(), bottomRight.lon()); - builder.endObject(); - builder.endObject(); + GeoBoundingBox bbox = resolveGeoBoundingBox(); + if (bbox != null) { + bbox.toXContent(builder, params); } return builder; } - private static class BoundingBox { - private final GeoPoint topLeft; - private final GeoPoint bottomRight; - - BoundingBox(GeoPoint topLeft, GeoPoint bottomRight) { - this.topLeft = topLeft; - this.bottomRight = bottomRight; - } - - public GeoPoint topLeft() { - return topLeft; - } - - public GeoPoint bottomRight() { - return bottomRight; - } - } - - private BoundingBox resolveBoundingBox() { + private GeoBoundingBox resolveGeoBoundingBox() { if (Double.isInfinite(top)) { return null; } else if (Double.isInfinite(posLeft)) { - return new BoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, negRight)); + return new GeoBoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, negRight)); } else if (Double.isInfinite(negLeft)) { - return new BoundingBox(new GeoPoint(top, posLeft), new GeoPoint(bottom, posRight)); + return new GeoBoundingBox(new GeoPoint(top, posLeft), new GeoPoint(bottom, posRight)); } else if (wrapLongitude) { double unwrappedWidth = posRight - negLeft; double wrappedWidth = (180 - posLeft) - (-180 - negRight); if (unwrappedWidth <= wrappedWidth) { - return new BoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, posRight)); + return new GeoBoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, posRight)); } else { - return new BoundingBox(new GeoPoint(top, posLeft), new GeoPoint(bottom, negRight)); + return new GeoBoundingBox(new GeoPoint(top, posLeft), new GeoPoint(bottom, negRight)); } } else { - return new BoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, posRight)); + return new GeoBoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, posRight)); } } @Override public GeoPoint topLeft() { - BoundingBox boundingBox = resolveBoundingBox(); - if (boundingBox == null) { + GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox(); + if (geoBoundingBox == null) { return null; } else { - return boundingBox.topLeft(); + return geoBoundingBox.topLeft(); } } @Override public GeoPoint bottomRight() { - BoundingBox boundingBox = resolveBoundingBox(); - if (boundingBox == null) { + GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox(); + if (geoBoundingBox == null) { return null; } else { - return boundingBox.bottomRight(); + return geoBoundingBox.bottomRight(); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java index 11d36d2ceeead..2b29ae15119d9 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java @@ -20,6 +20,7 @@ package org.elasticsearch.search.aggregations.metrics; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser; @@ -30,15 +31,14 @@ import java.io.IOException; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; -import static org.elasticsearch.search.aggregations.metrics.InternalGeoBounds.BOTTOM_RIGHT_FIELD; -import static org.elasticsearch.search.aggregations.metrics.InternalGeoBounds.BOUNDS_FIELD; -import static org.elasticsearch.search.aggregations.metrics.InternalGeoBounds.LAT_FIELD; -import static org.elasticsearch.search.aggregations.metrics.InternalGeoBounds.LON_FIELD; -import static org.elasticsearch.search.aggregations.metrics.InternalGeoBounds.TOP_LEFT_FIELD; +import static org.elasticsearch.common.geo.GeoBoundingBox.BOTTOM_RIGHT_FIELD; +import static org.elasticsearch.common.geo.GeoBoundingBox.BOUNDS_FIELD; +import static org.elasticsearch.common.geo.GeoBoundingBox.LAT_FIELD; +import static org.elasticsearch.common.geo.GeoBoundingBox.LON_FIELD; +import static org.elasticsearch.common.geo.GeoBoundingBox.TOP_LEFT_FIELD; public class ParsedGeoBounds extends ParsedAggregation implements GeoBounds { - private GeoPoint topLeft; - private GeoPoint bottomRight; + private GeoBoundingBox geoBoundingBox; @Override public String getType() { @@ -47,29 +47,20 @@ public String getType() { @Override public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { - if (topLeft != null) { - builder.startObject(BOUNDS_FIELD.getPreferredName()); - builder.startObject(TOP_LEFT_FIELD.getPreferredName()); - builder.field(LAT_FIELD.getPreferredName(), topLeft.getLat()); - builder.field(LON_FIELD.getPreferredName(), topLeft.getLon()); - builder.endObject(); - builder.startObject(BOTTOM_RIGHT_FIELD.getPreferredName()); - builder.field(LAT_FIELD.getPreferredName(), bottomRight.getLat()); - builder.field(LON_FIELD.getPreferredName(), bottomRight.getLon()); - builder.endObject(); - builder.endObject(); + if (geoBoundingBox != null) { + geoBoundingBox.toXContent(builder, params); } return builder; } @Override public GeoPoint topLeft() { - return topLeft; + return geoBoundingBox.topLeft(); } @Override public GeoPoint bottomRight() { - return bottomRight; + return geoBoundingBox.bottomRight(); } private static final ObjectParser PARSER = new ObjectParser<>(ParsedGeoBounds.class.getSimpleName(), true, @@ -85,8 +76,7 @@ public GeoPoint bottomRight() { static { declareAggregationFields(PARSER); PARSER.declareObject((agg, bbox) -> { - agg.topLeft = bbox.v1(); - agg.bottomRight = bbox.v2(); + agg.geoBoundingBox = new GeoBoundingBox(bbox.v1(), bbox.v2()); }, BOUNDS_PARSER, BOUNDS_FIELD); BOUNDS_PARSER.declareObject(constructorArg(), GEO_POINT_PARSER, TOP_LEFT_FIELD); diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeoBoundingBoxTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeoBoundingBoxTests.java new file mode 100644 index 0000000000000..ef705fa27c42c --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/geo/GeoBoundingBoxTests.java @@ -0,0 +1,149 @@ +/* + * 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. + */ + +package org.elasticsearch.common.geo; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.geo.GeometryTestUtils; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; + + +/** + * Tests for {@link GeoBoundingBox} + */ +public class GeoBoundingBoxTests extends ESTestCase { + + public void testInvalidParseInvalidWKT() throws IOException { + XContentBuilder bboxBuilder = XContentFactory.jsonBuilder() + .startObject() + .field("wkt", "invalid") + .endObject(); + XContentParser parser = createParser(bboxBuilder); + parser.nextToken(); + ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> GeoBoundingBox.parseBoundingBox(parser)); + assertThat(e.getMessage(), equalTo("failed to parse WKT bounding box")); + } + + public void testInvalidParsePoint() throws IOException { + XContentBuilder bboxBuilder = XContentFactory.jsonBuilder() + .startObject() + .field("wkt", "POINT (100.0 100.0)") + .endObject(); + XContentParser parser = createParser(bboxBuilder); + parser.nextToken(); + ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> GeoBoundingBox.parseBoundingBox(parser)); + assertThat(e.getMessage(), equalTo("failed to parse WKT bounding box. [POINT] found. expected [ENVELOPE]")); + } + + public void testWKT() throws IOException { + GeoBoundingBox geoBoundingBox = randomBBox(); + assertBBox(geoBoundingBox, + XContentFactory.jsonBuilder() + .startObject() + .field("wkt", geoBoundingBox.toString()) + .endObject() + ); + } + + public void testTopBottomLeftRight() throws Exception { + GeoBoundingBox geoBoundingBox = randomBBox(); + assertBBox(geoBoundingBox, + XContentFactory.jsonBuilder() + .startObject() + .field("top", geoBoundingBox.top()) + .field("bottom", geoBoundingBox.bottom()) + .field("left", geoBoundingBox.left()) + .field("right", geoBoundingBox.right()) + .endObject() + ); + } + + public void testTopLeftBottomRight() throws Exception { + GeoBoundingBox geoBoundingBox = randomBBox(); + assertBBox(geoBoundingBox, + XContentFactory.jsonBuilder() + .startObject() + .field("top_left", geoBoundingBox.topLeft()) + .field("bottom_right", geoBoundingBox.bottomRight()) + .endObject() + ); + } + + public void testTopRightBottomLeft() throws Exception { + GeoBoundingBox geoBoundingBox = randomBBox(); + assertBBox(geoBoundingBox, + XContentFactory.jsonBuilder() + .startObject() + .field("top_right", new GeoPoint(geoBoundingBox.top(), geoBoundingBox.right())) + .field("bottom_left", new GeoPoint(geoBoundingBox.bottom(), geoBoundingBox.left())) + .endObject() + ); + } + + // test that no exception is thrown. BBOX parsing is not validated + public void testNullTopBottomLeftRight() throws Exception { + GeoBoundingBox geoBoundingBox = randomBBox(); + XContentBuilder builder = XContentFactory.jsonBuilder().startObject(); + for (String field : randomSubsetOf(List.of("top", "bottom", "left", "right"))) { + switch (field) { + case "top": + builder.field("top", geoBoundingBox.top()); + break; + case "bottom": + builder.field("bottom", geoBoundingBox.bottom()); + break; + case "left": + builder.field("left", geoBoundingBox.left()); + break; + case "right": + builder.field("right", geoBoundingBox.right()); + break; + default: + throw new IllegalStateException("unexpected branching"); + } + } + builder.endObject(); + try (XContentParser parser = createParser(builder)) { + parser.nextToken(); + GeoBoundingBox.parseBoundingBox(parser); + } + } + + private void assertBBox(GeoBoundingBox expected, XContentBuilder builder) throws IOException { + try (XContentParser parser = createParser(builder)) { + parser.nextToken(); + assertThat(GeoBoundingBox.parseBoundingBox(parser), equalTo(expected)); + } + } + + private GeoBoundingBox randomBBox() { + double topLat = GeometryTestUtils.randomLat(); + double bottomLat = randomDoubleBetween(GeoUtils.MIN_LAT, topLat, true); + return new GeoBoundingBox(new GeoPoint(topLat, GeometryTestUtils.randomLon()), + new GeoPoint(bottomLat, GeometryTestUtils.randomLon())); + } +} diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoBoundingBoxIT.java b/server/src/test/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryIT.java similarity index 99% rename from server/src/test/java/org/elasticsearch/search/geo/GeoBoundingBoxIT.java rename to server/src/test/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryIT.java index 6e3f853e90eab..fe7800e137c9c 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoBoundingBoxIT.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryIT.java @@ -39,7 +39,7 @@ import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; -public class GeoBoundingBoxIT extends ESIntegTestCase { +public class GeoBoundingBoxQueryIT extends ESIntegTestCase { @Override protected boolean forbidPrivateIndexSettings() { From 3e4fc5db04dda62ebd08465ec8df74f1a7a39c51 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 23 Dec 2019 13:11:31 -0500 Subject: [PATCH 322/686] [DOCS] Percentile aggs are non-deterministic (#50468) Percentile aggregations are non-deterministic. A percentile aggregation can produce different results even when using the same data. Based on [this discuss post][0], the non-deterministic property stems from processes in Lucene that can affect the order in which docs are provided to the aggregation. This adds a warning stating that the aggregation is non-deterministic and what that means. [0]: https://discuss.elastic.co/t/different-results-for-same-query/111757 --- .../aggregations/metrics/percentile-aggregation.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/reference/aggregations/metrics/percentile-aggregation.asciidoc b/docs/reference/aggregations/metrics/percentile-aggregation.asciidoc index 9d4476c779997..e5744fec768b0 100644 --- a/docs/reference/aggregations/metrics/percentile-aggregation.asciidoc +++ b/docs/reference/aggregations/metrics/percentile-aggregation.asciidoc @@ -251,6 +251,13 @@ for large number of values is that the law of large numbers makes the distributi values more and more uniform and the t-digest tree can do a better job at summarizing it. It would not be the case on more skewed distributions. +[WARNING] +==== +Percentile aggregations are also +https://en.wikipedia.org/wiki/Nondeterministic_algorithm[non-deterministic]. +This means you can get slightly different results using the same data. +==== + [[search-aggregations-metrics-percentile-aggregation-compression]] ==== Compression From a5365f7098251a8852b87aaf45b561aebac28988 Mon Sep 17 00:00:00 2001 From: Orhan Toy Date: Mon, 23 Dec 2019 20:38:37 +0100 Subject: [PATCH 323/686] [DOCS] Fixes "enables you to" typos (#50225) --- docs/reference/docs/update.asciidoc | 2 +- .../ml/anomaly-detection/apis/validate-detector.asciidoc | 2 +- .../reference/ml/anomaly-detection/apis/validate-job.asciidoc | 2 +- docs/reference/upgrade/reindex_upgrade.asciidoc | 4 ++-- x-pack/docs/en/watcher/input/chain.asciidoc | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/reference/docs/update.asciidoc b/docs/reference/docs/update.asciidoc index 5394eb56c6a11..8d8e37be6a497 100644 --- a/docs/reference/docs/update.asciidoc +++ b/docs/reference/docs/update.asciidoc @@ -14,7 +14,7 @@ Updates a document using the specified script. [[update-api-desc]] ==== {api-description-title} -Enables you script document updates. The script can update, delete, or skip +Enables you to script document updates. The script can update, delete, or skip modifying the document. The update API also supports passing a partial document, which is merged into the existing document. To fully replace an existing document, use the <>. diff --git a/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc b/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc index 41d7e1e479c1c..cb811f247a1c7 100644 --- a/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc @@ -23,7 +23,7 @@ Validates detector configuration information. [[ml-valid-detector-desc]] ==== {api-description-title} -This API enables you validate the detector configuration +This API enables you to validate the detector configuration before you create an {anomaly-job}. [[ml-valid-detector-request-body]] diff --git a/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc index a741242922d20..750af88339c20 100644 --- a/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc @@ -23,7 +23,7 @@ Validates {anomaly-job} configuration information. [[ml-valid-job-desc]] ==== {api-description-title} -This API enables you validate the {anomaly-job} configuration before you +This API enables you to validate the {anomaly-job} configuration before you create the job. [[ml-valid-job-request-body]] diff --git a/docs/reference/upgrade/reindex_upgrade.asciidoc b/docs/reference/upgrade/reindex_upgrade.asciidoc index 7f576bd42700e..a9b1f60800706 100644 --- a/docs/reference/upgrade/reindex_upgrade.asciidoc +++ b/docs/reference/upgrade/reindex_upgrade.asciidoc @@ -102,8 +102,8 @@ endif::include-xpack[] === Reindex from a remote cluster You can use <> to migrate indices from -your old cluster to a new {version} cluster. This enables you move to {version} -from a pre-6.8 cluster without interrupting service. +your old cluster to a new {version} cluster. This enables you to move to +{version} from a pre-6.8 cluster without interrupting service. [WARNING] ============================================= diff --git a/x-pack/docs/en/watcher/input/chain.asciidoc b/x-pack/docs/en/watcher/input/chain.asciidoc index 7ff39d97d2906..69afe31046360 100644 --- a/x-pack/docs/en/watcher/input/chain.asciidoc +++ b/x-pack/docs/en/watcher/input/chain.asciidoc @@ -7,7 +7,7 @@ execution context when the watch is triggered. The inputs in a chain are processed in order and the data loaded by an input can be accessed by the subsequent inputs in the chain. -The `chain` input enables you perform actions based on data from multiple +The `chain` input enables you to perform actions based on data from multiple sources. You can also use the data collected by one input to load data from another source. From 4f613b80a1e88c9de62b2ab2d6bc3fcfa9986b69 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 23 Dec 2019 14:46:47 -0500 Subject: [PATCH 324/686] Fix name for eclipse formatter in CONTRIBUTING (#50470) The CONTRIBUTING.md file calls the Eclipse formatter `elasticsearch.eclipseformat.xml` but it looks like we call it `.eclipseformat.xml`. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd3bbfb1bf5e0..d8948af2a430e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -218,7 +218,7 @@ Please follow these formatting guidelines: #### Editor / IDE Support -Eclipse IDEs can import the file [elasticsearch.eclipseformat.xml] +Eclipse IDEs can import the file [.eclipseformat.xml] directly. IntelliJ IDEs can From 7c3c001e6dc97c98825ac9e151dd21a9c40e0b2d Mon Sep 17 00:00:00 2001 From: j-bean Date: Tue, 24 Dec 2019 12:49:21 +0300 Subject: [PATCH 325/686] Add remote info to the HLRC (#49657) Relates to #47678 --- .../elasticsearch/client/ClusterClient.java | 31 ++++ .../client/ClusterRequestConverters.java | 5 + .../client/cluster/ProxyModeInfo.java | 75 ++++++++++ .../client/cluster/RemoteConnectionInfo.java | 137 ++++++++++++++++++ .../client/cluster/RemoteInfoRequest.java | 28 ++++ .../client/cluster/RemoteInfoResponse.java | 59 ++++++++ .../client/cluster/SniffModeInfo.java | 76 ++++++++++ .../java/org/elasticsearch/client/CCRIT.java | 31 +--- .../elasticsearch/client/ClusterClientIT.java | 45 ++++++ .../client/ClusterRequestConvertersTests.java | 10 ++ .../client/ESRestHighLevelClientTestCase.java | 37 +++++ .../client/RestHighLevelClientTests.java | 10 +- .../cluster/RemoteInfoResponseTests.java | 111 ++++++++++++++ .../documentation/CCRDocumentationIT.java | 26 +--- .../ClusterClientDocumentationIT.java | 60 ++++++++ .../documentation/ILMDocumentationIT.java | 8 - .../high-level/cluster/remote_info.asciidoc | 32 ++++ .../high-level/supported-apis.asciidoc | 2 + .../cluster/remote/RemoteInfoResponse.java | 2 +- .../transport/ProxyConnectionStrategy.java | 16 +- .../transport/RemoteConnectionInfo.java | 14 +- .../transport/SniffConnectionStrategy.java | 16 +- .../org/elasticsearch/test/ESTestCase.java | 13 ++ 23 files changed, 770 insertions(+), 74 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java create mode 100644 docs/java-rest/high-level/cluster/remote_info.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java index 5e99975f51491..14fbf3e1f6d0f 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java @@ -26,6 +26,8 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; +import org.elasticsearch.client.cluster.RemoteInfoRequest; +import org.elasticsearch.client.cluster.RemoteInfoResponse; import org.elasticsearch.rest.RestStatus; import java.io.IOException; @@ -138,4 +140,33 @@ public Cancellable healthAsync(ClusterHealthRequest healthRequest, RequestOption return restHighLevelClient.performRequestAsyncAndParseEntity(healthRequest, ClusterRequestConverters::clusterHealth, options, ClusterHealthResponse::fromXContent, listener, singleton(RestStatus.REQUEST_TIMEOUT.getStatus())); } + + /** + * Get the remote cluster information using the Remote cluster info API. + * See Remote cluster info + * API on elastic.co + * @param request the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public RemoteInfoResponse remoteInfo(RemoteInfoRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, ClusterRequestConverters::remoteInfo, options, + RemoteInfoResponse::fromXContent, singleton(RestStatus.REQUEST_TIMEOUT.getStatus())); + } + + /** + * Asynchronously get remote cluster information using the Remote cluster info API. + * See Remote cluster info + * API on elastic.co + * @param request the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + * @return cancellable that may be used to cancel the request + */ + public Cancellable remoteInfoAsync(RemoteInfoRequest request, RequestOptions options, + ActionListener listener) { + return restHighLevelClient.performRequestAsyncAndParseEntity(request, ClusterRequestConverters::remoteInfo, options, + RemoteInfoResponse::fromXContent, listener, singleton(RestStatus.REQUEST_TIMEOUT.getStatus())); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java index a246402b505cc..74b2c3b7c6aae 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java @@ -25,6 +25,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.support.ActiveShardCount; +import org.elasticsearch.client.cluster.RemoteInfoRequest; import org.elasticsearch.common.Strings; import java.io.IOException; @@ -76,4 +77,8 @@ static Request clusterHealth(ClusterHealthRequest healthRequest) { request.addParameters(params.asMap()); return request; } + + static Request remoteInfo(RemoteInfoRequest remoteInfoRequest) { + return new Request(HttpGet.METHOD_NAME, "/_remote/info"); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java new file mode 100644 index 0000000000000..0fc4f240eb8ef --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java @@ -0,0 +1,75 @@ +/* + * 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. + */ + +package org.elasticsearch.client.cluster; + +import java.util.Objects; + +public class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { + static final String NAME = "proxy"; + static final String ADDRESS = "address"; + static final String NUM_SOCKETS_CONNECTED = "num_sockets_connected"; + static final String MAX_SOCKET_CONNECTIONS = "max_socket_connections"; + private final String address; + private final int maxSocketConnections; + private final int numSocketsConnected; + + ProxyModeInfo(String address, int maxSocketConnections, int numSocketsConnected) { + this.address = address; + this.maxSocketConnections = maxSocketConnections; + this.numSocketsConnected = numSocketsConnected; + } + + @Override + public boolean isConnected() { + return numSocketsConnected > 0; + } + + @Override + public String modeName() { + return NAME; + } + + public String getAddress() { + return address; + } + + public int getMaxSocketConnections() { + return maxSocketConnections; + } + + public int getNumSocketsConnected() { + return numSocketsConnected; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProxyModeInfo otherProxy = (ProxyModeInfo) o; + return maxSocketConnections == otherProxy.maxSocketConnections && + numSocketsConnected == otherProxy.numSocketsConnected && + Objects.equals(address, otherProxy.address); + } + + @Override + public int hashCode() { + return Objects.hash(address, maxSocketConnections, numSocketsConnected); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java new file mode 100644 index 0000000000000..9f3efc0b4899e --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java @@ -0,0 +1,137 @@ +/* + * 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. + */ + +package org.elasticsearch.client.cluster; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * This class encapsulates all remote cluster information to be rendered on + * {@code _remote/info} requests. + */ +public final class RemoteConnectionInfo { + private static final String CONNECTED = "connected"; + private static final String MODE = "mode"; + private static final String INITIAL_CONNECT_TIMEOUT = "initial_connect_timeout"; + private static final String SKIP_UNAVAILABLE = "skip_unavailable"; + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "RemoteConnectionInfoObjectParser", + false, + (args, clusterAlias) -> { + String mode = (String) args[1]; + ModeInfo modeInfo; + if (mode.equals(ProxyModeInfo.NAME)) { + modeInfo = new ProxyModeInfo((String) args[4], (int) args[5], (int) args[6]); + } else if (mode.equals(SniffModeInfo.NAME)) { + modeInfo = new SniffModeInfo((List) args[7], (int) args[8], (int) args[9]); + } else { + throw new IllegalArgumentException("mode cannot be " + mode); + } + return new RemoteConnectionInfo(clusterAlias, + modeInfo, + TimeValue.parseTimeValue((String) args[2], INITIAL_CONNECT_TIMEOUT), + (boolean) args[3]); + }); + + static { + PARSER.declareBoolean(constructorArg(), new ParseField(CONNECTED)); + PARSER.declareString(constructorArg(), new ParseField(MODE)); + PARSER.declareString(constructorArg(), new ParseField(INITIAL_CONNECT_TIMEOUT)); + PARSER.declareBoolean(constructorArg(), new ParseField(SKIP_UNAVAILABLE)); + + PARSER.declareString(optionalConstructorArg(), new ParseField(ProxyModeInfo.ADDRESS)); + PARSER.declareInt(optionalConstructorArg(), new ParseField(ProxyModeInfo.MAX_SOCKET_CONNECTIONS)); + PARSER.declareInt(optionalConstructorArg(), new ParseField(ProxyModeInfo.NUM_SOCKETS_CONNECTED)); + + PARSER.declareStringArray(optionalConstructorArg(), new ParseField(SniffModeInfo.SEEDS)); + PARSER.declareInt(optionalConstructorArg(), new ParseField(SniffModeInfo.MAX_CONNECTIONS_PER_CLUSTER)); + PARSER.declareInt(optionalConstructorArg(), new ParseField(SniffModeInfo.NUM_NODES_CONNECTED)); + } + + final ModeInfo modeInfo; + final TimeValue initialConnectionTimeout; + final String clusterAlias; + final boolean skipUnavailable; + + RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, TimeValue initialConnectionTimeout, boolean skipUnavailable) { + this.clusterAlias = clusterAlias; + this.modeInfo = modeInfo; + this.initialConnectionTimeout = initialConnectionTimeout; + this.skipUnavailable = skipUnavailable; + } + + public boolean isConnected() { + return modeInfo.isConnected(); + } + + public String getClusterAlias() { + return clusterAlias; + } + + public ModeInfo getModeInfo() { + return modeInfo; + } + + public TimeValue getInitialConnectionTimeout() { + return initialConnectionTimeout; + } + + public boolean isSkipUnavailable() { + return skipUnavailable; + } + + public static RemoteConnectionInfo fromXContent(XContentParser parser, String clusterAlias) throws IOException { + return PARSER.parse(parser, clusterAlias); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RemoteConnectionInfo that = (RemoteConnectionInfo) o; + return skipUnavailable == that.skipUnavailable && + Objects.equals(modeInfo, that.modeInfo) && + Objects.equals(initialConnectionTimeout, that.initialConnectionTimeout) && + Objects.equals(clusterAlias, that.clusterAlias); + } + + @Override + public int hashCode() { + return Objects.hash(modeInfo, initialConnectionTimeout, clusterAlias, skipUnavailable); + } + + public interface ModeInfo { + + boolean isConnected(); + + String modeName(); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java new file mode 100644 index 0000000000000..5ffc8afc073c6 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java @@ -0,0 +1,28 @@ +/* + * 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. + */ +package org.elasticsearch.client.cluster; + +import org.elasticsearch.client.Validatable; + +/** + * The request object used by the Remote cluster info API. + */ +public final class RemoteInfoRequest implements Validatable { + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java new file mode 100644 index 0000000000000..cc453049667b1 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java @@ -0,0 +1,59 @@ +/* + * 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. + */ +package org.elasticsearch.client.cluster; + +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * A response to _remote/info API request. + */ +public final class RemoteInfoResponse { + + private List infos; + + RemoteInfoResponse(Collection infos) { + this.infos = List.copyOf(infos); + } + + public List getInfos() { + return infos; + } + + public static RemoteInfoResponse fromXContent(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + + List infos = new ArrayList<>(); + + XContentParser.Token token; + while ((token = parser.nextToken()) == XContentParser.Token.FIELD_NAME) { + String clusterAlias = parser.currentName(); + RemoteConnectionInfo info = RemoteConnectionInfo.fromXContent(parser, clusterAlias); + infos.add(info); + } + ensureExpectedToken(XContentParser.Token.END_OBJECT, token, parser::getTokenLocation); + return new RemoteInfoResponse(infos); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java new file mode 100644 index 0000000000000..b0e75979975ee --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java @@ -0,0 +1,76 @@ +/* + * 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. + */ + +package org.elasticsearch.client.cluster; + +import java.util.List; +import java.util.Objects; + +public class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { + public static final String NAME = "sniff"; + static final String SEEDS = "seeds"; + static final String NUM_NODES_CONNECTED = "num_nodes_connected"; + static final String MAX_CONNECTIONS_PER_CLUSTER = "max_connections_per_cluster"; + final List seedNodes; + final int maxConnectionsPerCluster; + final int numNodesConnected; + + SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { + this.seedNodes = seedNodes; + this.maxConnectionsPerCluster = maxConnectionsPerCluster; + this.numNodesConnected = numNodesConnected; + } + + @Override + public boolean isConnected() { + return numNodesConnected > 0; + } + + @Override + public String modeName() { + return NAME; + } + + public List getSeedNodes() { + return seedNodes; + } + + public int getMaxConnectionsPerCluster() { + return maxConnectionsPerCluster; + } + + public int getNumNodesConnected() { + return numNodesConnected; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SniffModeInfo sniff = (SniffModeInfo) o; + return maxConnectionsPerCluster == sniff.maxConnectionsPerCluster && + numNodesConnected == sniff.numNodesConnected && + Objects.equals(seedNodes, sniff.seedNodes); + } + + @Override + public int hashCode() { + return Objects.hash(seedNodes, maxConnectionsPerCluster, numNodesConnected); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java index 6be2efe98c48b..c6678102fe2b1 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java @@ -19,10 +19,7 @@ package org.elasticsearch.client; -import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; @@ -51,9 +48,7 @@ import org.elasticsearch.client.indices.CloseIndexRequest; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.CreateIndexResponse; -import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.test.rest.yaml.ObjectPath; import org.junit.Before; @@ -74,27 +69,7 @@ public class CCRIT extends ESRestHighLevelClientTestCase { @Before public void setupRemoteClusterConfig() throws Exception { - // Configure local cluster as remote cluster: - // TODO: replace with nodes info highlevel rest client code when it is available: - final Request request = new Request("GET", "/_nodes"); - Map nodesResponse = (Map) toMap(client().performRequest(request)).get("nodes"); - // Select node info of first node (we don't know the node id): - nodesResponse = (Map) nodesResponse.get(nodesResponse.keySet().iterator().next()); - String transportAddress = (String) nodesResponse.get("transport_address"); - - ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); - updateSettingsRequest.transientSettings(Collections.singletonMap("cluster.remote.local_cluster.seeds", transportAddress)); - ClusterUpdateSettingsResponse updateSettingsResponse = - highLevelClient().cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); - assertThat(updateSettingsResponse.isAcknowledged(), is(true)); - - assertBusy(() -> { - Map localConnection = (Map) toMap(client() - .performRequest(new Request("GET", "/_remote/info"))) - .get("local_cluster"); - assertThat(localConnection, notNullValue()); - assertThat(localConnection.get("connected"), is(true)); - }); + setupRemoteClusterConfig("local_cluster"); } public void testIndexFollowing() throws Exception { @@ -311,8 +286,4 @@ public void testAutoFollowing() throws Exception { assertThat(pauseFollowResponse.isAcknowledged(), is(true)); } - private static Map toMap(Response response) throws IOException { - return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); - } - } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java index e78e7ec7ca6d3..471a6dc697795 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java @@ -27,19 +27,27 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; +import org.elasticsearch.client.cluster.RemoteConnectionInfo; +import org.elasticsearch.client.cluster.RemoteInfoRequest; +import org.elasticsearch.client.cluster.RemoteInfoResponse; +import org.elasticsearch.client.cluster.SniffModeInfo; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.health.ClusterIndexHealth; import org.elasticsearch.cluster.health.ClusterShardHealth; import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.indices.recovery.RecoverySettings; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.transport.RemoteClusterService; +import org.elasticsearch.transport.SniffConnectionStrategy; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import static java.util.Collections.emptyMap; @@ -297,4 +305,41 @@ public void testClusterHealthNotFoundIndex() throws IOException { assertNoIndices(response); } + public void testRemoteInfo() throws Exception { + String clusterAlias = "local_cluster"; + setupRemoteClusterConfig(clusterAlias); + + ClusterGetSettingsRequest settingsRequest = new ClusterGetSettingsRequest(); + settingsRequest.includeDefaults(true); + ClusterGetSettingsResponse settingsResponse = highLevelClient().cluster().getSettings(settingsRequest, RequestOptions.DEFAULT); + + List seeds = SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS + .getConcreteSettingForNamespace(clusterAlias) + .get(settingsResponse.getTransientSettings()); + int connectionsPerCluster = SniffConnectionStrategy.REMOTE_CONNECTIONS_PER_CLUSTER + .get(settingsResponse.getDefaultSettings()); + TimeValue initialConnectionTimeout = RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING + .get(settingsResponse.getDefaultSettings()); + boolean skipUnavailable = RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE + .getConcreteSettingForNamespace(clusterAlias) + .get(settingsResponse.getDefaultSettings()); + + RemoteInfoRequest request = new RemoteInfoRequest(); + RemoteInfoResponse response = execute(request, highLevelClient().cluster()::remoteInfo, + highLevelClient().cluster()::remoteInfoAsync); + + assertThat(response, notNullValue()); + assertThat(response.getInfos().size(), equalTo(1)); + RemoteConnectionInfo info = response.getInfos().get(0); + assertThat(info.getClusterAlias(), equalTo(clusterAlias)); + assertThat(info.getInitialConnectionTimeout(), equalTo(initialConnectionTimeout)); + assertThat(info.isSkipUnavailable(), equalTo(skipUnavailable)); + assertThat(info.getModeInfo().modeName(), equalTo(SniffModeInfo.NAME)); + assertThat(info.getModeInfo().isConnected(), equalTo(true)); + SniffModeInfo sniffModeInfo = (SniffModeInfo) info.getModeInfo(); + assertThat(sniffModeInfo.getMaxConnectionsPerCluster(), equalTo(connectionsPerCluster)); + assertThat(sniffModeInfo.getNumNodesConnected(), equalTo(1)); + assertThat(sniffModeInfo.getSeedNodes(), equalTo(seeds)); + } + } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java index 9b7b5b0d284dc..59bf52a677321 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java @@ -26,6 +26,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.master.AcknowledgedRequest; +import org.elasticsearch.client.cluster.RemoteInfoRequest; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.Priority; import org.elasticsearch.test.ESTestCase; @@ -37,6 +38,7 @@ import java.util.Locale; import java.util.Map; +import static java.util.Collections.emptyMap; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.nullValue; @@ -147,4 +149,12 @@ public void testClusterHealth() { } Assert.assertThat(request.getParameters(), equalTo(expectedParams)); } + + public void testRemoteInfo() { + RemoteInfoRequest request = new RemoteInfoRequest(); + Request expectedRequest = ClusterRequestConverters.remoteInfo(request); + assertEquals("/_remote/info", expectedRequest.getEndpoint()); + assertEquals(HttpGet.METHOD_NAME, expectedRequest.getMethod()); + assertEquals(emptyMap(), expectedRequest.getParameters()); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java index a9012fda042d9..174c98f3642df 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java @@ -19,19 +19,25 @@ package org.elasticsearch.client; +import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; import org.elasticsearch.action.ingest.PutPipelineRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.cluster.RemoteInfoRequest; +import org.elasticsearch.client.cluster.RemoteInfoResponse; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.common.Booleans; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.ingest.Pipeline; import org.elasticsearch.search.SearchHit; @@ -45,10 +51,15 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collections; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +import static java.util.Collections.singletonMap; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public abstract class ESRestHighLevelClientTestCase extends ESRestTestCase { @@ -243,4 +254,30 @@ protected void createIndexWithMultipleShards(String index) throws IOException { ); highLevelClient().indices().create(indexRequest, RequestOptions.DEFAULT); } + + protected static void setupRemoteClusterConfig(String remoteClusterName) throws Exception { + // Configure local cluster as remote cluster: + // TODO: replace with nodes info highlevel rest client code when it is available: + final Request request = new Request("GET", "/_nodes"); + Map nodesResponse = (Map) toMap(client().performRequest(request)).get("nodes"); + // Select node info of first node (we don't know the node id): + nodesResponse = (Map) nodesResponse.get(nodesResponse.keySet().iterator().next()); + String transportAddress = (String) nodesResponse.get("transport_address"); + + ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); + updateSettingsRequest.transientSettings(singletonMap("cluster.remote." + remoteClusterName + ".seeds", transportAddress)); + ClusterUpdateSettingsResponse updateSettingsResponse = + restHighLevelClient.cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); + assertThat(updateSettingsResponse.isAcknowledged(), is(true)); + + assertBusy(() -> { + RemoteInfoResponse response = highLevelClient().cluster().remoteInfo(new RemoteInfoRequest(), RequestOptions.DEFAULT); + assertThat(response, notNullValue()); + assertThat(response.getInfos().size(), greaterThan(0)); + }); + } + + protected static Map toMap(Response response) throws IOException { + return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index 4039ec251d335..b3457eedb5569 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -59,8 +59,8 @@ import org.elasticsearch.client.ml.dataframe.OutlierDetection; import org.elasticsearch.client.ml.dataframe.evaluation.classification.AccuracyMetric; import org.elasticsearch.client.ml.dataframe.evaluation.classification.Classification; -import org.elasticsearch.client.ml.dataframe.evaluation.regression.MeanSquaredErrorMetric; import org.elasticsearch.client.ml.dataframe.evaluation.classification.MulticlassConfusionMatrixMetric; +import org.elasticsearch.client.ml.dataframe.evaluation.regression.MeanSquaredErrorMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.RSquaredMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.Regression; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.AucRocMetric; @@ -68,14 +68,14 @@ import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.ConfusionMatrixMetric; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.PrecisionMetric; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.RecallMetric; +import org.elasticsearch.client.ml.inference.preprocessing.FrequencyEncoding; +import org.elasticsearch.client.ml.inference.preprocessing.OneHotEncoding; +import org.elasticsearch.client.ml.inference.preprocessing.TargetMeanEncoding; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.Ensemble; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.LogisticRegression; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.WeightedMode; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.WeightedSum; import org.elasticsearch.client.ml.inference.trainedmodel.tree.Tree; -import org.elasticsearch.client.ml.inference.preprocessing.FrequencyEncoding; -import org.elasticsearch.client.ml.inference.preprocessing.OneHotEncoding; -import org.elasticsearch.client.ml.inference.preprocessing.TargetMeanEncoding; import org.elasticsearch.client.transform.transforms.SyncConfig; import org.elasticsearch.client.transform.transforms.TimeSyncConfig; import org.elasticsearch.common.CheckedFunction; @@ -106,7 +106,6 @@ import org.elasticsearch.test.InternalAggregationTestCase; import org.elasticsearch.test.rest.yaml.restspec.ClientYamlSuiteRestApi; import org.elasticsearch.test.rest.yaml.restspec.ClientYamlSuiteRestSpec; - import org.hamcrest.Matchers; import org.junit.Before; @@ -773,7 +772,6 @@ public void testProvidedNamedXContents() { public void testApiNamingConventions() throws Exception { //this list should be empty once the high-level client is feature complete String[] notYetSupportedApi = new String[]{ - "cluster.remote_info", "create", "get_script_context", "get_script_languages", diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java new file mode 100644 index 0000000000000..6280bc9e11470 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java @@ -0,0 +1,111 @@ +/* + * 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. + */ + +package org.elasticsearch.client.cluster; + +import org.elasticsearch.client.AbstractResponseTestCase; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.transport.ProxyConnectionStrategy; +import org.elasticsearch.transport.RemoteConnectionInfo; +import org.elasticsearch.transport.SniffConnectionStrategy; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static org.hamcrest.Matchers.equalTo; + +public class RemoteInfoResponseTests extends AbstractResponseTestCase { + + @Override + protected org.elasticsearch.action.admin.cluster.remote.RemoteInfoResponse createServerTestInstance(XContentType xContentType) { + int numRemoteInfos = randomIntBetween(0, 8); + List remoteInfos = new ArrayList<>(); + for (int i = 0; i < numRemoteInfos; i++) { + remoteInfos.add(createRandomRemoteConnectionInfo()); + } + return new org.elasticsearch.action.admin.cluster.remote.RemoteInfoResponse(remoteInfos); + } + + @Override + protected RemoteInfoResponse doParseToClientInstance(XContentParser parser) throws IOException { + return RemoteInfoResponse.fromXContent(parser); + } + + @Override + protected void assertInstances(org.elasticsearch.action.admin.cluster.remote.RemoteInfoResponse serverTestInstance, + RemoteInfoResponse clientInstance) { + assertThat(clientInstance.getInfos().size(), equalTo(serverTestInstance.getInfos().size())); + Map serverInfos = serverTestInstance.getInfos().stream() + .collect(toMap(RemoteConnectionInfo::getClusterAlias, identity())); + for (org.elasticsearch.client.cluster.RemoteConnectionInfo clientRemoteInfo : clientInstance.getInfos()) { + RemoteConnectionInfo serverRemoteInfo = serverInfos.get(clientRemoteInfo.getClusterAlias()); + assertThat(clientRemoteInfo.getClusterAlias(), equalTo(serverRemoteInfo.getClusterAlias())); + assertThat(clientRemoteInfo.getInitialConnectionTimeout(), equalTo(serverRemoteInfo.getInitialConnectionTimeout())); + assertThat(clientRemoteInfo.isConnected(), equalTo(serverRemoteInfo.isConnected())); + assertThat(clientRemoteInfo.isSkipUnavailable(), equalTo(serverRemoteInfo.isSkipUnavailable())); + assertThat(clientRemoteInfo.getModeInfo().isConnected(), equalTo(serverRemoteInfo.getModeInfo().isConnected())); + assertThat(clientRemoteInfo.getModeInfo().modeName(), equalTo(serverRemoteInfo.getModeInfo().modeName())); + if (clientRemoteInfo.getModeInfo().modeName().equals(SniffModeInfo.NAME)) { + SniffModeInfo clientModeInfo = + (SniffModeInfo) clientRemoteInfo.getModeInfo(); + SniffConnectionStrategy.SniffModeInfo serverModeInfo = + (SniffConnectionStrategy.SniffModeInfo) serverRemoteInfo.getModeInfo(); + assertThat(clientModeInfo.getMaxConnectionsPerCluster(), equalTo(serverModeInfo.getMaxConnectionsPerCluster())); + assertThat(clientModeInfo.getNumNodesConnected(), equalTo(serverModeInfo.getNumNodesConnected())); + assertThat(clientModeInfo.getSeedNodes(), equalTo(serverModeInfo.getSeedNodes())); + } else if (clientRemoteInfo.getModeInfo().modeName().equals(ProxyModeInfo.NAME)) { + ProxyModeInfo clientModeInfo = + (ProxyModeInfo) clientRemoteInfo.getModeInfo(); + ProxyConnectionStrategy.ProxyModeInfo serverModeInfo = + (ProxyConnectionStrategy.ProxyModeInfo) serverRemoteInfo.getModeInfo(); + assertThat(clientModeInfo.getAddress(), equalTo(serverModeInfo.getAddress())); + assertThat(clientModeInfo.getMaxSocketConnections(), equalTo(serverModeInfo.getMaxSocketConnections())); + assertThat(clientModeInfo.getNumSocketsConnected(), equalTo(serverModeInfo.getNumSocketsConnected())); + } else { + fail("impossible case"); + } + } + } + + private static RemoteConnectionInfo createRandomRemoteConnectionInfo() { + RemoteConnectionInfo.ModeInfo modeInfo; + if (randomBoolean()) { + String address = randomAlphaOfLength(8); + int maxSocketConnections = randomInt(5); + int numSocketsConnected = randomInt(5); + modeInfo = new ProxyConnectionStrategy.ProxyModeInfo(address, maxSocketConnections, numSocketsConnected); + } else { + List seedNodes = randomList(randomInt(8), () -> randomAlphaOfLength(8)); + int maxConnectionsPerCluster = randomInt(5); + int numNodesConnected = randomInt(5); + modeInfo = new SniffConnectionStrategy.SniffModeInfo(seedNodes, maxConnectionsPerCluster, numNodesConnected); + } + String clusterAlias = randomAlphaOfLength(8); + TimeValue initialConnectionTimeout = TimeValue.parseTimeValue(randomTimeValue(), "randomInitialConnectionTimeout"); + boolean skipUnavailable = randomBoolean(); + return new RemoteConnectionInfo(clusterAlias, modeInfo, initialConnectionTimeout, skipUnavailable); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java index 48f7adda8445c..5e7da8142129d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java @@ -19,11 +19,8 @@ package org.elasticsearch.client.documentation; -import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.client.ESRestHighLevelClientTestCase; @@ -57,8 +54,6 @@ import org.elasticsearch.client.indices.CloseIndexRequest; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.CreateIndexResponse; -import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.rest.yaml.ObjectPath; import org.junit.Before; @@ -76,21 +71,8 @@ public class CCRDocumentationIT extends ESRestHighLevelClientTestCase { @Before - public void setupRemoteClusterConfig() throws IOException { - RestHighLevelClient client = highLevelClient(); - // Configure local cluster as remote cluster: - // TODO: replace with nodes info highlevel rest client code when it is available: - final Request request = new Request("GET", "/_nodes"); - Map nodesResponse = (Map) toMap(client().performRequest(request)).get("nodes"); - // Select node info of first node (we don't know the node id): - nodesResponse = (Map) nodesResponse.get(nodesResponse.keySet().iterator().next()); - String transportAddress = (String) nodesResponse.get("transport_address"); - - ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); - updateSettingsRequest.transientSettings(Collections.singletonMap("cluster.remote.local.seeds", transportAddress)); - ClusterUpdateSettingsResponse updateSettingsResponse = - client.cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); - assertThat(updateSettingsResponse.isAcknowledged(), is(true)); + public void setupRemoteClusterConfig() throws Exception { + setupRemoteClusterConfig("local"); } public void testPutFollow() throws Exception { @@ -985,8 +967,4 @@ public void onFailure(Exception e) { } } - static Map toMap(Response response) throws IOException { - return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); - } - } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java index 8b7f1577114b0..5cc7a4424fcaa 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java @@ -31,6 +31,9 @@ import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.cluster.RemoteConnectionInfo; +import org.elasticsearch.client.cluster.RemoteInfoRequest; +import org.elasticsearch.client.cluster.RemoteInfoResponse; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.health.ClusterIndexHealth; @@ -46,6 +49,7 @@ import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -415,4 +419,60 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } + + public void testRemoteInfo() throws Exception { + setupRemoteClusterConfig("local_cluster"); + + RestHighLevelClient client = highLevelClient(); + + // tag::remote-info-request + RemoteInfoRequest request = new RemoteInfoRequest(); + // end::remote-info-request + + // tag::remote-info-execute + RemoteInfoResponse response = client.cluster().remoteInfo(request, RequestOptions.DEFAULT); // <1> + // end::remote-info-execute + + // tag::remote-info-response + List infos = response.getInfos(); + // end::remote-info-response + + assertThat(infos.size(), greaterThan(0)); + } + + public void testRemoteInfoAsync() throws Exception { + setupRemoteClusterConfig("local_cluster"); + + RestHighLevelClient client = highLevelClient(); + + // tag::remote-info-request + RemoteInfoRequest request = new RemoteInfoRequest(); + // end::remote-info-request + + + // tag::remote-info-execute-listener + ActionListener listener = + new ActionListener<>() { + @Override + public void onResponse(RemoteInfoResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::remote-info-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::health-execute-async + client.cluster().remoteInfoAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::health-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java index a9852c405a49d..a458c3ec2551d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java @@ -19,7 +19,6 @@ package org.elasticsearch.client.documentation; -import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; @@ -28,7 +27,6 @@ import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.RequestOptions; -import org.elasticsearch.client.Response; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.core.AcknowledgedResponse; import org.elasticsearch.client.ilm.DeleteAction; @@ -78,8 +76,6 @@ import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.snapshots.SnapshotState; @@ -1210,8 +1206,4 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } - static Map toMap(Response response) throws IOException { - return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); - } - } diff --git a/docs/java-rest/high-level/cluster/remote_info.asciidoc b/docs/java-rest/high-level/cluster/remote_info.asciidoc new file mode 100644 index 0000000000000..6496a04a3a76c --- /dev/null +++ b/docs/java-rest/high-level/cluster/remote_info.asciidoc @@ -0,0 +1,32 @@ +-- +:api: remote-info +:request: RemoteInfoRequest +:response: RemoteInfoResponse +-- + +[id="{upid}-{api}"] +=== Remote Cluster Info API + +The Remote cluster info API allows to get all of the configured remote cluster information. + +[id="{upid}-{api}-request"] +==== Remote Cluster Info Request + +A +{request}+: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- + +There are no required parameters. + +==== Remote Cluster Info Response + +The returned +{response}+ allows to retrieve remote cluster information. +It returns connection and endpoint information keyed by the configured remote cluster alias. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 2191e795ebbb1..e0d228b5d1e4b 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -168,12 +168,14 @@ The Java High Level REST Client supports the following Cluster APIs: * <> * <> * <> +* <> :upid: {mainid}-cluster :doc-tests-file: {doc-tests}/ClusterClientDocumentationIT.java include::cluster/put_settings.asciidoc[] include::cluster/get_settings.asciidoc[] include::cluster/health.asciidoc[] +include::cluster/remote_info.asciidoc[] == Ingest APIs The Java High Level REST Client supports the following Ingest APIs: diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java index 894cd9cf9fec4..9d72da7746974 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java @@ -39,7 +39,7 @@ public final class RemoteInfoResponse extends ActionResponse implements ToXConte infos = in.readList(RemoteConnectionInfo::new); } - RemoteInfoResponse(Collection infos) { + public RemoteInfoResponse(Collection infos) { this.infos = List.copyOf(infos); } diff --git a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java index 78c9f5f28154d..14ca217ade1b8 100644 --- a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java @@ -268,13 +268,13 @@ private static TransportAddress resolveAddress(String address) { return new TransportAddress(parseConfiguredAddress(address)); } - static class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { + public static class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { private final String address; private final int maxSocketConnections; private final int numSocketsConnected; - ProxyModeInfo(String address, int maxSocketConnections, int numSocketsConnected) { + public ProxyModeInfo(String address, int maxSocketConnections, int numSocketsConnected) { this.address = address; this.maxSocketConnections = maxSocketConnections; this.numSocketsConnected = numSocketsConnected; @@ -311,6 +311,18 @@ public String modeName() { return "proxy"; } + public String getAddress() { + return address; + } + + public int getMaxSocketConnections() { + return maxSocketConnections; + } + + public int getNumSocketsConnected() { + return numSocketsConnected; + } + @Override public RemoteConnectionStrategy.ConnectionStrategy modeType() { return RemoteConnectionStrategy.ConnectionStrategy.PROXY; diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java index e721a0b617fd1..152cafccb61e0 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java @@ -43,7 +43,7 @@ public final class RemoteConnectionInfo implements ToXContentFragment, Writeable final String clusterAlias; final boolean skipUnavailable; - RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, TimeValue initialConnectionTimeout, boolean skipUnavailable) { + public RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, TimeValue initialConnectionTimeout, boolean skipUnavailable) { this.clusterAlias = clusterAlias; this.modeInfo = modeInfo; this.initialConnectionTimeout = initialConnectionTimeout; @@ -77,6 +77,18 @@ public String getClusterAlias() { return clusterAlias; } + public ModeInfo getModeInfo() { + return modeInfo; + } + + public TimeValue getInitialConnectionTimeout() { + return initialConnectionTimeout; + } + + public boolean isSkipUnavailable() { + return skipUnavailable; + } + @Override public void writeTo(StreamOutput out) throws IOException { // TODO: Change to 7.6 after backport diff --git a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java index 97b5e318841d6..fcc4bde951dd0 100644 --- a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java @@ -465,13 +465,13 @@ private boolean proxyChanged(String oldProxy, String newProxy) { return Objects.equals(oldProxy, newProxy) == false; } - static class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { + public static class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { final List seedNodes; final int maxConnectionsPerCluster; final int numNodesConnected; - SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { + public SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { this.seedNodes = seedNodes; this.maxConnectionsPerCluster = maxConnectionsPerCluster; this.numNodesConnected = numNodesConnected; @@ -512,6 +512,18 @@ public String modeName() { return "sniff"; } + public List getSeedNodes() { + return seedNodes; + } + + public int getMaxConnectionsPerCluster() { + return maxConnectionsPerCluster; + } + + public int getNumNodesConnected() { + return numNodesConnected; + } + @Override public RemoteConnectionStrategy.ConnectionStrategy modeType() { return RemoteConnectionStrategy.ConnectionStrategy.SNIFF; diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index 4d1848e5b3a98..e4c9eb0986b34 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -755,6 +755,19 @@ public static T[] randomArray(int minArraySize, int maxArraySize, IntFunctio return array; } + public static List randomList(int maxListSize, Supplier valueConstructor) { + return randomList(0, maxListSize, valueConstructor); + } + + public static List randomList(int minListSize, int maxListSize, Supplier valueConstructor) { + final int size = randomIntBetween(minListSize, maxListSize); + List list = new ArrayList<>(); + for (int i = 0; i < size; i++) { + list.add(valueConstructor.get()); + } + return list; + } + private static final String[] TIME_SUFFIXES = new String[]{"d", "h", "ms", "s", "m", "micros", "nanos"}; From 9e287a8c673a0121d9e29233c2ab3526783174db Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 24 Dec 2019 11:26:10 +0100 Subject: [PATCH 326/686] serialize initial_connect_timeout as xcontent correctly --- .../java/org/elasticsearch/transport/RemoteConnectionInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java index 152cafccb61e0..ef7c76113269f 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java @@ -121,7 +121,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("connected", modeInfo.isConnected()); builder.field("mode", modeInfo.modeName()); modeInfo.toXContent(builder, params); - builder.field("initial_connect_timeout", initialConnectionTimeout); + builder.field("initial_connect_timeout", initialConnectionTimeout.getStringRep()); builder.field("skip_unavailable", skipUnavailable); } builder.endObject(); From c5ff357a59ddbb10d6e2495cde0c3ad7daa43079 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 24 Dec 2019 11:39:22 +0100 Subject: [PATCH 327/686] Revert "serialize initial_connect_timeout as xcontent correctly" This reverts commit ae64eaabdae001f91013c703ebe72d9acfc0cd13. --- .../java/org/elasticsearch/transport/RemoteConnectionInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java index ef7c76113269f..152cafccb61e0 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java @@ -121,7 +121,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("connected", modeInfo.isConnected()); builder.field("mode", modeInfo.modeName()); modeInfo.toXContent(builder, params); - builder.field("initial_connect_timeout", initialConnectionTimeout.getStringRep()); + builder.field("initial_connect_timeout", initialConnectionTimeout); builder.field("skip_unavailable", skipUnavailable); } builder.endObject(); From d911f1834a45cb1dca5cc45d8599f27d8e582df9 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 24 Dec 2019 11:40:44 +0100 Subject: [PATCH 328/686] Revert "Add remote info to the HLRC (#49657)" This reverts commit fa1a7c57b8e6fc6ecfc77a3811d4746087049b7e. --- .../elasticsearch/client/ClusterClient.java | 31 ---- .../client/ClusterRequestConverters.java | 5 - .../client/cluster/ProxyModeInfo.java | 75 ---------- .../client/cluster/RemoteConnectionInfo.java | 137 ------------------ .../client/cluster/RemoteInfoRequest.java | 28 ---- .../client/cluster/RemoteInfoResponse.java | 59 -------- .../client/cluster/SniffModeInfo.java | 76 ---------- .../java/org/elasticsearch/client/CCRIT.java | 31 +++- .../elasticsearch/client/ClusterClientIT.java | 45 ------ .../client/ClusterRequestConvertersTests.java | 10 -- .../client/ESRestHighLevelClientTestCase.java | 37 ----- .../client/RestHighLevelClientTests.java | 10 +- .../cluster/RemoteInfoResponseTests.java | 111 -------------- .../documentation/CCRDocumentationIT.java | 26 +++- .../ClusterClientDocumentationIT.java | 60 -------- .../documentation/ILMDocumentationIT.java | 8 + .../high-level/cluster/remote_info.asciidoc | 32 ---- .../high-level/supported-apis.asciidoc | 2 - .../cluster/remote/RemoteInfoResponse.java | 2 +- .../transport/ProxyConnectionStrategy.java | 16 +- .../transport/RemoteConnectionInfo.java | 14 +- .../transport/SniffConnectionStrategy.java | 16 +- .../org/elasticsearch/test/ESTestCase.java | 13 -- 23 files changed, 74 insertions(+), 770 deletions(-) delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java delete mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java delete mode 100644 docs/java-rest/high-level/cluster/remote_info.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java index 14fbf3e1f6d0f..5e99975f51491 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java @@ -26,8 +26,6 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; -import org.elasticsearch.client.cluster.RemoteInfoRequest; -import org.elasticsearch.client.cluster.RemoteInfoResponse; import org.elasticsearch.rest.RestStatus; import java.io.IOException; @@ -140,33 +138,4 @@ public Cancellable healthAsync(ClusterHealthRequest healthRequest, RequestOption return restHighLevelClient.performRequestAsyncAndParseEntity(healthRequest, ClusterRequestConverters::clusterHealth, options, ClusterHealthResponse::fromXContent, listener, singleton(RestStatus.REQUEST_TIMEOUT.getStatus())); } - - /** - * Get the remote cluster information using the Remote cluster info API. - * See Remote cluster info - * API on elastic.co - * @param request the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response - */ - public RemoteInfoResponse remoteInfo(RemoteInfoRequest request, RequestOptions options) throws IOException { - return restHighLevelClient.performRequestAndParseEntity(request, ClusterRequestConverters::remoteInfo, options, - RemoteInfoResponse::fromXContent, singleton(RestStatus.REQUEST_TIMEOUT.getStatus())); - } - - /** - * Asynchronously get remote cluster information using the Remote cluster info API. - * See Remote cluster info - * API on elastic.co - * @param request the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @param listener the listener to be notified upon request completion - * @return cancellable that may be used to cancel the request - */ - public Cancellable remoteInfoAsync(RemoteInfoRequest request, RequestOptions options, - ActionListener listener) { - return restHighLevelClient.performRequestAsyncAndParseEntity(request, ClusterRequestConverters::remoteInfo, options, - RemoteInfoResponse::fromXContent, listener, singleton(RestStatus.REQUEST_TIMEOUT.getStatus())); - } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java index 74b2c3b7c6aae..a246402b505cc 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java @@ -25,7 +25,6 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.support.ActiveShardCount; -import org.elasticsearch.client.cluster.RemoteInfoRequest; import org.elasticsearch.common.Strings; import java.io.IOException; @@ -77,8 +76,4 @@ static Request clusterHealth(ClusterHealthRequest healthRequest) { request.addParameters(params.asMap()); return request; } - - static Request remoteInfo(RemoteInfoRequest remoteInfoRequest) { - return new Request(HttpGet.METHOD_NAME, "/_remote/info"); - } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java deleted file mode 100644 index 0fc4f240eb8ef..0000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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. - */ - -package org.elasticsearch.client.cluster; - -import java.util.Objects; - -public class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { - static final String NAME = "proxy"; - static final String ADDRESS = "address"; - static final String NUM_SOCKETS_CONNECTED = "num_sockets_connected"; - static final String MAX_SOCKET_CONNECTIONS = "max_socket_connections"; - private final String address; - private final int maxSocketConnections; - private final int numSocketsConnected; - - ProxyModeInfo(String address, int maxSocketConnections, int numSocketsConnected) { - this.address = address; - this.maxSocketConnections = maxSocketConnections; - this.numSocketsConnected = numSocketsConnected; - } - - @Override - public boolean isConnected() { - return numSocketsConnected > 0; - } - - @Override - public String modeName() { - return NAME; - } - - public String getAddress() { - return address; - } - - public int getMaxSocketConnections() { - return maxSocketConnections; - } - - public int getNumSocketsConnected() { - return numSocketsConnected; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ProxyModeInfo otherProxy = (ProxyModeInfo) o; - return maxSocketConnections == otherProxy.maxSocketConnections && - numSocketsConnected == otherProxy.numSocketsConnected && - Objects.equals(address, otherProxy.address); - } - - @Override - public int hashCode() { - return Objects.hash(address, maxSocketConnections, numSocketsConnected); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java deleted file mode 100644 index 9f3efc0b4899e..0000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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. - */ - -package org.elasticsearch.client.cluster; - -import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.ConstructingObjectParser; -import org.elasticsearch.common.xcontent.XContentParser; - -import java.io.IOException; -import java.util.List; -import java.util.Objects; - -import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; -import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; - -/** - * This class encapsulates all remote cluster information to be rendered on - * {@code _remote/info} requests. - */ -public final class RemoteConnectionInfo { - private static final String CONNECTED = "connected"; - private static final String MODE = "mode"; - private static final String INITIAL_CONNECT_TIMEOUT = "initial_connect_timeout"; - private static final String SKIP_UNAVAILABLE = "skip_unavailable"; - - @SuppressWarnings("unchecked") - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "RemoteConnectionInfoObjectParser", - false, - (args, clusterAlias) -> { - String mode = (String) args[1]; - ModeInfo modeInfo; - if (mode.equals(ProxyModeInfo.NAME)) { - modeInfo = new ProxyModeInfo((String) args[4], (int) args[5], (int) args[6]); - } else if (mode.equals(SniffModeInfo.NAME)) { - modeInfo = new SniffModeInfo((List) args[7], (int) args[8], (int) args[9]); - } else { - throw new IllegalArgumentException("mode cannot be " + mode); - } - return new RemoteConnectionInfo(clusterAlias, - modeInfo, - TimeValue.parseTimeValue((String) args[2], INITIAL_CONNECT_TIMEOUT), - (boolean) args[3]); - }); - - static { - PARSER.declareBoolean(constructorArg(), new ParseField(CONNECTED)); - PARSER.declareString(constructorArg(), new ParseField(MODE)); - PARSER.declareString(constructorArg(), new ParseField(INITIAL_CONNECT_TIMEOUT)); - PARSER.declareBoolean(constructorArg(), new ParseField(SKIP_UNAVAILABLE)); - - PARSER.declareString(optionalConstructorArg(), new ParseField(ProxyModeInfo.ADDRESS)); - PARSER.declareInt(optionalConstructorArg(), new ParseField(ProxyModeInfo.MAX_SOCKET_CONNECTIONS)); - PARSER.declareInt(optionalConstructorArg(), new ParseField(ProxyModeInfo.NUM_SOCKETS_CONNECTED)); - - PARSER.declareStringArray(optionalConstructorArg(), new ParseField(SniffModeInfo.SEEDS)); - PARSER.declareInt(optionalConstructorArg(), new ParseField(SniffModeInfo.MAX_CONNECTIONS_PER_CLUSTER)); - PARSER.declareInt(optionalConstructorArg(), new ParseField(SniffModeInfo.NUM_NODES_CONNECTED)); - } - - final ModeInfo modeInfo; - final TimeValue initialConnectionTimeout; - final String clusterAlias; - final boolean skipUnavailable; - - RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, TimeValue initialConnectionTimeout, boolean skipUnavailable) { - this.clusterAlias = clusterAlias; - this.modeInfo = modeInfo; - this.initialConnectionTimeout = initialConnectionTimeout; - this.skipUnavailable = skipUnavailable; - } - - public boolean isConnected() { - return modeInfo.isConnected(); - } - - public String getClusterAlias() { - return clusterAlias; - } - - public ModeInfo getModeInfo() { - return modeInfo; - } - - public TimeValue getInitialConnectionTimeout() { - return initialConnectionTimeout; - } - - public boolean isSkipUnavailable() { - return skipUnavailable; - } - - public static RemoteConnectionInfo fromXContent(XContentParser parser, String clusterAlias) throws IOException { - return PARSER.parse(parser, clusterAlias); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RemoteConnectionInfo that = (RemoteConnectionInfo) o; - return skipUnavailable == that.skipUnavailable && - Objects.equals(modeInfo, that.modeInfo) && - Objects.equals(initialConnectionTimeout, that.initialConnectionTimeout) && - Objects.equals(clusterAlias, that.clusterAlias); - } - - @Override - public int hashCode() { - return Objects.hash(modeInfo, initialConnectionTimeout, clusterAlias, skipUnavailable); - } - - public interface ModeInfo { - - boolean isConnected(); - - String modeName(); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java deleted file mode 100644 index 5ffc8afc073c6..0000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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. - */ -package org.elasticsearch.client.cluster; - -import org.elasticsearch.client.Validatable; - -/** - * The request object used by the Remote cluster info API. - */ -public final class RemoteInfoRequest implements Validatable { - -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java deleted file mode 100644 index cc453049667b1..0000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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. - */ -package org.elasticsearch.client.cluster; - -import org.elasticsearch.common.xcontent.XContentParser; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; - -/** - * A response to _remote/info API request. - */ -public final class RemoteInfoResponse { - - private List infos; - - RemoteInfoResponse(Collection infos) { - this.infos = List.copyOf(infos); - } - - public List getInfos() { - return infos; - } - - public static RemoteInfoResponse fromXContent(XContentParser parser) throws IOException { - ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); - - List infos = new ArrayList<>(); - - XContentParser.Token token; - while ((token = parser.nextToken()) == XContentParser.Token.FIELD_NAME) { - String clusterAlias = parser.currentName(); - RemoteConnectionInfo info = RemoteConnectionInfo.fromXContent(parser, clusterAlias); - infos.add(info); - } - ensureExpectedToken(XContentParser.Token.END_OBJECT, token, parser::getTokenLocation); - return new RemoteInfoResponse(infos); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java deleted file mode 100644 index b0e75979975ee..0000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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. - */ - -package org.elasticsearch.client.cluster; - -import java.util.List; -import java.util.Objects; - -public class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { - public static final String NAME = "sniff"; - static final String SEEDS = "seeds"; - static final String NUM_NODES_CONNECTED = "num_nodes_connected"; - static final String MAX_CONNECTIONS_PER_CLUSTER = "max_connections_per_cluster"; - final List seedNodes; - final int maxConnectionsPerCluster; - final int numNodesConnected; - - SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { - this.seedNodes = seedNodes; - this.maxConnectionsPerCluster = maxConnectionsPerCluster; - this.numNodesConnected = numNodesConnected; - } - - @Override - public boolean isConnected() { - return numNodesConnected > 0; - } - - @Override - public String modeName() { - return NAME; - } - - public List getSeedNodes() { - return seedNodes; - } - - public int getMaxConnectionsPerCluster() { - return maxConnectionsPerCluster; - } - - public int getNumNodesConnected() { - return numNodesConnected; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SniffModeInfo sniff = (SniffModeInfo) o; - return maxConnectionsPerCluster == sniff.maxConnectionsPerCluster && - numNodesConnected == sniff.numNodesConnected && - Objects.equals(seedNodes, sniff.seedNodes); - } - - @Override - public int hashCode() { - return Objects.hash(seedNodes, maxConnectionsPerCluster, numNodesConnected); - } -} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java index c6678102fe2b1..6be2efe98c48b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java @@ -19,7 +19,10 @@ package org.elasticsearch.client; +import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; @@ -48,7 +51,9 @@ import org.elasticsearch.client.indices.CloseIndexRequest; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.CreateIndexResponse; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.test.rest.yaml.ObjectPath; import org.junit.Before; @@ -69,7 +74,27 @@ public class CCRIT extends ESRestHighLevelClientTestCase { @Before public void setupRemoteClusterConfig() throws Exception { - setupRemoteClusterConfig("local_cluster"); + // Configure local cluster as remote cluster: + // TODO: replace with nodes info highlevel rest client code when it is available: + final Request request = new Request("GET", "/_nodes"); + Map nodesResponse = (Map) toMap(client().performRequest(request)).get("nodes"); + // Select node info of first node (we don't know the node id): + nodesResponse = (Map) nodesResponse.get(nodesResponse.keySet().iterator().next()); + String transportAddress = (String) nodesResponse.get("transport_address"); + + ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); + updateSettingsRequest.transientSettings(Collections.singletonMap("cluster.remote.local_cluster.seeds", transportAddress)); + ClusterUpdateSettingsResponse updateSettingsResponse = + highLevelClient().cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); + assertThat(updateSettingsResponse.isAcknowledged(), is(true)); + + assertBusy(() -> { + Map localConnection = (Map) toMap(client() + .performRequest(new Request("GET", "/_remote/info"))) + .get("local_cluster"); + assertThat(localConnection, notNullValue()); + assertThat(localConnection.get("connected"), is(true)); + }); } public void testIndexFollowing() throws Exception { @@ -286,4 +311,8 @@ public void testAutoFollowing() throws Exception { assertThat(pauseFollowResponse.isAcknowledged(), is(true)); } + private static Map toMap(Response response) throws IOException { + return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); + } + } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java index 471a6dc697795..e78e7ec7ca6d3 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java @@ -27,27 +27,19 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; -import org.elasticsearch.client.cluster.RemoteConnectionInfo; -import org.elasticsearch.client.cluster.RemoteInfoRequest; -import org.elasticsearch.client.cluster.RemoteInfoResponse; -import org.elasticsearch.client.cluster.SniffModeInfo; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.health.ClusterIndexHealth; import org.elasticsearch.cluster.health.ClusterShardHealth; import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; -import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.indices.recovery.RecoverySettings; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.transport.RemoteClusterService; -import org.elasticsearch.transport.SniffConnectionStrategy; import java.io.IOException; import java.util.HashMap; -import java.util.List; import java.util.Map; import static java.util.Collections.emptyMap; @@ -305,41 +297,4 @@ public void testClusterHealthNotFoundIndex() throws IOException { assertNoIndices(response); } - public void testRemoteInfo() throws Exception { - String clusterAlias = "local_cluster"; - setupRemoteClusterConfig(clusterAlias); - - ClusterGetSettingsRequest settingsRequest = new ClusterGetSettingsRequest(); - settingsRequest.includeDefaults(true); - ClusterGetSettingsResponse settingsResponse = highLevelClient().cluster().getSettings(settingsRequest, RequestOptions.DEFAULT); - - List seeds = SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS - .getConcreteSettingForNamespace(clusterAlias) - .get(settingsResponse.getTransientSettings()); - int connectionsPerCluster = SniffConnectionStrategy.REMOTE_CONNECTIONS_PER_CLUSTER - .get(settingsResponse.getDefaultSettings()); - TimeValue initialConnectionTimeout = RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING - .get(settingsResponse.getDefaultSettings()); - boolean skipUnavailable = RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE - .getConcreteSettingForNamespace(clusterAlias) - .get(settingsResponse.getDefaultSettings()); - - RemoteInfoRequest request = new RemoteInfoRequest(); - RemoteInfoResponse response = execute(request, highLevelClient().cluster()::remoteInfo, - highLevelClient().cluster()::remoteInfoAsync); - - assertThat(response, notNullValue()); - assertThat(response.getInfos().size(), equalTo(1)); - RemoteConnectionInfo info = response.getInfos().get(0); - assertThat(info.getClusterAlias(), equalTo(clusterAlias)); - assertThat(info.getInitialConnectionTimeout(), equalTo(initialConnectionTimeout)); - assertThat(info.isSkipUnavailable(), equalTo(skipUnavailable)); - assertThat(info.getModeInfo().modeName(), equalTo(SniffModeInfo.NAME)); - assertThat(info.getModeInfo().isConnected(), equalTo(true)); - SniffModeInfo sniffModeInfo = (SniffModeInfo) info.getModeInfo(); - assertThat(sniffModeInfo.getMaxConnectionsPerCluster(), equalTo(connectionsPerCluster)); - assertThat(sniffModeInfo.getNumNodesConnected(), equalTo(1)); - assertThat(sniffModeInfo.getSeedNodes(), equalTo(seeds)); - } - } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java index 59bf52a677321..9b7b5b0d284dc 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java @@ -26,7 +26,6 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.master.AcknowledgedRequest; -import org.elasticsearch.client.cluster.RemoteInfoRequest; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.Priority; import org.elasticsearch.test.ESTestCase; @@ -38,7 +37,6 @@ import java.util.Locale; import java.util.Map; -import static java.util.Collections.emptyMap; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.nullValue; @@ -149,12 +147,4 @@ public void testClusterHealth() { } Assert.assertThat(request.getParameters(), equalTo(expectedParams)); } - - public void testRemoteInfo() { - RemoteInfoRequest request = new RemoteInfoRequest(); - Request expectedRequest = ClusterRequestConverters.remoteInfo(request); - assertEquals("/_remote/info", expectedRequest.getEndpoint()); - assertEquals(HttpGet.METHOD_NAME, expectedRequest.getMethod()); - assertEquals(emptyMap(), expectedRequest.getParameters()); - } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java index 174c98f3642df..a9012fda042d9 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java @@ -19,25 +19,19 @@ package org.elasticsearch.client; -import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; import org.elasticsearch.action.ingest.PutPipelineRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.PlainActionFuture; -import org.elasticsearch.client.cluster.RemoteInfoRequest; -import org.elasticsearch.client.cluster.RemoteInfoResponse; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.common.Booleans; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.ingest.Pipeline; import org.elasticsearch.search.SearchHit; @@ -51,15 +45,10 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collections; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import static java.util.Collections.singletonMap; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; public abstract class ESRestHighLevelClientTestCase extends ESRestTestCase { @@ -254,30 +243,4 @@ protected void createIndexWithMultipleShards(String index) throws IOException { ); highLevelClient().indices().create(indexRequest, RequestOptions.DEFAULT); } - - protected static void setupRemoteClusterConfig(String remoteClusterName) throws Exception { - // Configure local cluster as remote cluster: - // TODO: replace with nodes info highlevel rest client code when it is available: - final Request request = new Request("GET", "/_nodes"); - Map nodesResponse = (Map) toMap(client().performRequest(request)).get("nodes"); - // Select node info of first node (we don't know the node id): - nodesResponse = (Map) nodesResponse.get(nodesResponse.keySet().iterator().next()); - String transportAddress = (String) nodesResponse.get("transport_address"); - - ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); - updateSettingsRequest.transientSettings(singletonMap("cluster.remote." + remoteClusterName + ".seeds", transportAddress)); - ClusterUpdateSettingsResponse updateSettingsResponse = - restHighLevelClient.cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); - assertThat(updateSettingsResponse.isAcknowledged(), is(true)); - - assertBusy(() -> { - RemoteInfoResponse response = highLevelClient().cluster().remoteInfo(new RemoteInfoRequest(), RequestOptions.DEFAULT); - assertThat(response, notNullValue()); - assertThat(response.getInfos().size(), greaterThan(0)); - }); - } - - protected static Map toMap(Response response) throws IOException { - return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); - } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index b3457eedb5569..4039ec251d335 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -59,8 +59,8 @@ import org.elasticsearch.client.ml.dataframe.OutlierDetection; import org.elasticsearch.client.ml.dataframe.evaluation.classification.AccuracyMetric; import org.elasticsearch.client.ml.dataframe.evaluation.classification.Classification; -import org.elasticsearch.client.ml.dataframe.evaluation.classification.MulticlassConfusionMatrixMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.MeanSquaredErrorMetric; +import org.elasticsearch.client.ml.dataframe.evaluation.classification.MulticlassConfusionMatrixMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.RSquaredMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.Regression; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.AucRocMetric; @@ -68,14 +68,14 @@ import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.ConfusionMatrixMetric; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.PrecisionMetric; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.RecallMetric; -import org.elasticsearch.client.ml.inference.preprocessing.FrequencyEncoding; -import org.elasticsearch.client.ml.inference.preprocessing.OneHotEncoding; -import org.elasticsearch.client.ml.inference.preprocessing.TargetMeanEncoding; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.Ensemble; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.LogisticRegression; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.WeightedMode; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.WeightedSum; import org.elasticsearch.client.ml.inference.trainedmodel.tree.Tree; +import org.elasticsearch.client.ml.inference.preprocessing.FrequencyEncoding; +import org.elasticsearch.client.ml.inference.preprocessing.OneHotEncoding; +import org.elasticsearch.client.ml.inference.preprocessing.TargetMeanEncoding; import org.elasticsearch.client.transform.transforms.SyncConfig; import org.elasticsearch.client.transform.transforms.TimeSyncConfig; import org.elasticsearch.common.CheckedFunction; @@ -106,6 +106,7 @@ import org.elasticsearch.test.InternalAggregationTestCase; import org.elasticsearch.test.rest.yaml.restspec.ClientYamlSuiteRestApi; import org.elasticsearch.test.rest.yaml.restspec.ClientYamlSuiteRestSpec; + import org.hamcrest.Matchers; import org.junit.Before; @@ -772,6 +773,7 @@ public void testProvidedNamedXContents() { public void testApiNamingConventions() throws Exception { //this list should be empty once the high-level client is feature complete String[] notYetSupportedApi = new String[]{ + "cluster.remote_info", "create", "get_script_context", "get_script_languages", diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java deleted file mode 100644 index 6280bc9e11470..0000000000000 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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. - */ - -package org.elasticsearch.client.cluster; - -import org.elasticsearch.client.AbstractResponseTestCase; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.transport.ProxyConnectionStrategy; -import org.elasticsearch.transport.RemoteConnectionInfo; -import org.elasticsearch.transport.SniffConnectionStrategy; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; -import static org.hamcrest.Matchers.equalTo; - -public class RemoteInfoResponseTests extends AbstractResponseTestCase { - - @Override - protected org.elasticsearch.action.admin.cluster.remote.RemoteInfoResponse createServerTestInstance(XContentType xContentType) { - int numRemoteInfos = randomIntBetween(0, 8); - List remoteInfos = new ArrayList<>(); - for (int i = 0; i < numRemoteInfos; i++) { - remoteInfos.add(createRandomRemoteConnectionInfo()); - } - return new org.elasticsearch.action.admin.cluster.remote.RemoteInfoResponse(remoteInfos); - } - - @Override - protected RemoteInfoResponse doParseToClientInstance(XContentParser parser) throws IOException { - return RemoteInfoResponse.fromXContent(parser); - } - - @Override - protected void assertInstances(org.elasticsearch.action.admin.cluster.remote.RemoteInfoResponse serverTestInstance, - RemoteInfoResponse clientInstance) { - assertThat(clientInstance.getInfos().size(), equalTo(serverTestInstance.getInfos().size())); - Map serverInfos = serverTestInstance.getInfos().stream() - .collect(toMap(RemoteConnectionInfo::getClusterAlias, identity())); - for (org.elasticsearch.client.cluster.RemoteConnectionInfo clientRemoteInfo : clientInstance.getInfos()) { - RemoteConnectionInfo serverRemoteInfo = serverInfos.get(clientRemoteInfo.getClusterAlias()); - assertThat(clientRemoteInfo.getClusterAlias(), equalTo(serverRemoteInfo.getClusterAlias())); - assertThat(clientRemoteInfo.getInitialConnectionTimeout(), equalTo(serverRemoteInfo.getInitialConnectionTimeout())); - assertThat(clientRemoteInfo.isConnected(), equalTo(serverRemoteInfo.isConnected())); - assertThat(clientRemoteInfo.isSkipUnavailable(), equalTo(serverRemoteInfo.isSkipUnavailable())); - assertThat(clientRemoteInfo.getModeInfo().isConnected(), equalTo(serverRemoteInfo.getModeInfo().isConnected())); - assertThat(clientRemoteInfo.getModeInfo().modeName(), equalTo(serverRemoteInfo.getModeInfo().modeName())); - if (clientRemoteInfo.getModeInfo().modeName().equals(SniffModeInfo.NAME)) { - SniffModeInfo clientModeInfo = - (SniffModeInfo) clientRemoteInfo.getModeInfo(); - SniffConnectionStrategy.SniffModeInfo serverModeInfo = - (SniffConnectionStrategy.SniffModeInfo) serverRemoteInfo.getModeInfo(); - assertThat(clientModeInfo.getMaxConnectionsPerCluster(), equalTo(serverModeInfo.getMaxConnectionsPerCluster())); - assertThat(clientModeInfo.getNumNodesConnected(), equalTo(serverModeInfo.getNumNodesConnected())); - assertThat(clientModeInfo.getSeedNodes(), equalTo(serverModeInfo.getSeedNodes())); - } else if (clientRemoteInfo.getModeInfo().modeName().equals(ProxyModeInfo.NAME)) { - ProxyModeInfo clientModeInfo = - (ProxyModeInfo) clientRemoteInfo.getModeInfo(); - ProxyConnectionStrategy.ProxyModeInfo serverModeInfo = - (ProxyConnectionStrategy.ProxyModeInfo) serverRemoteInfo.getModeInfo(); - assertThat(clientModeInfo.getAddress(), equalTo(serverModeInfo.getAddress())); - assertThat(clientModeInfo.getMaxSocketConnections(), equalTo(serverModeInfo.getMaxSocketConnections())); - assertThat(clientModeInfo.getNumSocketsConnected(), equalTo(serverModeInfo.getNumSocketsConnected())); - } else { - fail("impossible case"); - } - } - } - - private static RemoteConnectionInfo createRandomRemoteConnectionInfo() { - RemoteConnectionInfo.ModeInfo modeInfo; - if (randomBoolean()) { - String address = randomAlphaOfLength(8); - int maxSocketConnections = randomInt(5); - int numSocketsConnected = randomInt(5); - modeInfo = new ProxyConnectionStrategy.ProxyModeInfo(address, maxSocketConnections, numSocketsConnected); - } else { - List seedNodes = randomList(randomInt(8), () -> randomAlphaOfLength(8)); - int maxConnectionsPerCluster = randomInt(5); - int numNodesConnected = randomInt(5); - modeInfo = new SniffConnectionStrategy.SniffModeInfo(seedNodes, maxConnectionsPerCluster, numNodesConnected); - } - String clusterAlias = randomAlphaOfLength(8); - TimeValue initialConnectionTimeout = TimeValue.parseTimeValue(randomTimeValue(), "randomInitialConnectionTimeout"); - boolean skipUnavailable = randomBoolean(); - return new RemoteConnectionInfo(clusterAlias, modeInfo, initialConnectionTimeout, skipUnavailable); - } -} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java index 5e7da8142129d..48f7adda8445c 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java @@ -19,8 +19,11 @@ package org.elasticsearch.client.documentation; +import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.client.ESRestHighLevelClientTestCase; @@ -54,6 +57,8 @@ import org.elasticsearch.client.indices.CloseIndexRequest; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.CreateIndexResponse; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.rest.yaml.ObjectPath; import org.junit.Before; @@ -71,8 +76,21 @@ public class CCRDocumentationIT extends ESRestHighLevelClientTestCase { @Before - public void setupRemoteClusterConfig() throws Exception { - setupRemoteClusterConfig("local"); + public void setupRemoteClusterConfig() throws IOException { + RestHighLevelClient client = highLevelClient(); + // Configure local cluster as remote cluster: + // TODO: replace with nodes info highlevel rest client code when it is available: + final Request request = new Request("GET", "/_nodes"); + Map nodesResponse = (Map) toMap(client().performRequest(request)).get("nodes"); + // Select node info of first node (we don't know the node id): + nodesResponse = (Map) nodesResponse.get(nodesResponse.keySet().iterator().next()); + String transportAddress = (String) nodesResponse.get("transport_address"); + + ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); + updateSettingsRequest.transientSettings(Collections.singletonMap("cluster.remote.local.seeds", transportAddress)); + ClusterUpdateSettingsResponse updateSettingsResponse = + client.cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); + assertThat(updateSettingsResponse.isAcknowledged(), is(true)); } public void testPutFollow() throws Exception { @@ -967,4 +985,8 @@ public void onFailure(Exception e) { } } + static Map toMap(Response response) throws IOException { + return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); + } + } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java index 5cc7a4424fcaa..8b7f1577114b0 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java @@ -31,9 +31,6 @@ import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; -import org.elasticsearch.client.cluster.RemoteConnectionInfo; -import org.elasticsearch.client.cluster.RemoteInfoRequest; -import org.elasticsearch.client.cluster.RemoteInfoResponse; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.health.ClusterIndexHealth; @@ -49,7 +46,6 @@ import java.io.IOException; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -419,60 +415,4 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } - - public void testRemoteInfo() throws Exception { - setupRemoteClusterConfig("local_cluster"); - - RestHighLevelClient client = highLevelClient(); - - // tag::remote-info-request - RemoteInfoRequest request = new RemoteInfoRequest(); - // end::remote-info-request - - // tag::remote-info-execute - RemoteInfoResponse response = client.cluster().remoteInfo(request, RequestOptions.DEFAULT); // <1> - // end::remote-info-execute - - // tag::remote-info-response - List infos = response.getInfos(); - // end::remote-info-response - - assertThat(infos.size(), greaterThan(0)); - } - - public void testRemoteInfoAsync() throws Exception { - setupRemoteClusterConfig("local_cluster"); - - RestHighLevelClient client = highLevelClient(); - - // tag::remote-info-request - RemoteInfoRequest request = new RemoteInfoRequest(); - // end::remote-info-request - - - // tag::remote-info-execute-listener - ActionListener listener = - new ActionListener<>() { - @Override - public void onResponse(RemoteInfoResponse response) { - // <1> - } - - @Override - public void onFailure(Exception e) { - // <2> - } - }; - // end::remote-info-execute-listener - - // Replace the empty listener by a blocking listener in test - final CountDownLatch latch = new CountDownLatch(1); - listener = new LatchedActionListener<>(listener, latch); - - // tag::health-execute-async - client.cluster().remoteInfoAsync(request, RequestOptions.DEFAULT, listener); // <1> - // end::health-execute-async - - assertTrue(latch.await(30L, TimeUnit.SECONDS)); - } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java index a458c3ec2551d..a9852c405a49d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.client.documentation; +import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; @@ -27,6 +28,7 @@ import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.core.AcknowledgedResponse; import org.elasticsearch.client.ilm.DeleteAction; @@ -76,6 +78,8 @@ import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.snapshots.SnapshotState; @@ -1206,4 +1210,8 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } + static Map toMap(Response response) throws IOException { + return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); + } + } diff --git a/docs/java-rest/high-level/cluster/remote_info.asciidoc b/docs/java-rest/high-level/cluster/remote_info.asciidoc deleted file mode 100644 index 6496a04a3a76c..0000000000000 --- a/docs/java-rest/high-level/cluster/remote_info.asciidoc +++ /dev/null @@ -1,32 +0,0 @@ --- -:api: remote-info -:request: RemoteInfoRequest -:response: RemoteInfoResponse --- - -[id="{upid}-{api}"] -=== Remote Cluster Info API - -The Remote cluster info API allows to get all of the configured remote cluster information. - -[id="{upid}-{api}-request"] -==== Remote Cluster Info Request - -A +{request}+: - -["source","java",subs="attributes,callouts,macros"] --------------------------------------------------- -include-tagged::{doc-tests-file}[{api}-request] --------------------------------------------------- - -There are no required parameters. - -==== Remote Cluster Info Response - -The returned +{response}+ allows to retrieve remote cluster information. -It returns connection and endpoint information keyed by the configured remote cluster alias. - -["source","java",subs="attributes,callouts,macros"] --------------------------------------------------- -include-tagged::{doc-tests-file}[{api}-response] --------------------------------------------------- diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index e0d228b5d1e4b..2191e795ebbb1 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -168,14 +168,12 @@ The Java High Level REST Client supports the following Cluster APIs: * <> * <> * <> -* <> :upid: {mainid}-cluster :doc-tests-file: {doc-tests}/ClusterClientDocumentationIT.java include::cluster/put_settings.asciidoc[] include::cluster/get_settings.asciidoc[] include::cluster/health.asciidoc[] -include::cluster/remote_info.asciidoc[] == Ingest APIs The Java High Level REST Client supports the following Ingest APIs: diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java index 9d72da7746974..894cd9cf9fec4 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java @@ -39,7 +39,7 @@ public final class RemoteInfoResponse extends ActionResponse implements ToXConte infos = in.readList(RemoteConnectionInfo::new); } - public RemoteInfoResponse(Collection infos) { + RemoteInfoResponse(Collection infos) { this.infos = List.copyOf(infos); } diff --git a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java index 14ca217ade1b8..78c9f5f28154d 100644 --- a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java @@ -268,13 +268,13 @@ private static TransportAddress resolveAddress(String address) { return new TransportAddress(parseConfiguredAddress(address)); } - public static class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { + static class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { private final String address; private final int maxSocketConnections; private final int numSocketsConnected; - public ProxyModeInfo(String address, int maxSocketConnections, int numSocketsConnected) { + ProxyModeInfo(String address, int maxSocketConnections, int numSocketsConnected) { this.address = address; this.maxSocketConnections = maxSocketConnections; this.numSocketsConnected = numSocketsConnected; @@ -311,18 +311,6 @@ public String modeName() { return "proxy"; } - public String getAddress() { - return address; - } - - public int getMaxSocketConnections() { - return maxSocketConnections; - } - - public int getNumSocketsConnected() { - return numSocketsConnected; - } - @Override public RemoteConnectionStrategy.ConnectionStrategy modeType() { return RemoteConnectionStrategy.ConnectionStrategy.PROXY; diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java index 152cafccb61e0..e721a0b617fd1 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java @@ -43,7 +43,7 @@ public final class RemoteConnectionInfo implements ToXContentFragment, Writeable final String clusterAlias; final boolean skipUnavailable; - public RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, TimeValue initialConnectionTimeout, boolean skipUnavailable) { + RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, TimeValue initialConnectionTimeout, boolean skipUnavailable) { this.clusterAlias = clusterAlias; this.modeInfo = modeInfo; this.initialConnectionTimeout = initialConnectionTimeout; @@ -77,18 +77,6 @@ public String getClusterAlias() { return clusterAlias; } - public ModeInfo getModeInfo() { - return modeInfo; - } - - public TimeValue getInitialConnectionTimeout() { - return initialConnectionTimeout; - } - - public boolean isSkipUnavailable() { - return skipUnavailable; - } - @Override public void writeTo(StreamOutput out) throws IOException { // TODO: Change to 7.6 after backport diff --git a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java index fcc4bde951dd0..97b5e318841d6 100644 --- a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java @@ -465,13 +465,13 @@ private boolean proxyChanged(String oldProxy, String newProxy) { return Objects.equals(oldProxy, newProxy) == false; } - public static class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { + static class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { final List seedNodes; final int maxConnectionsPerCluster; final int numNodesConnected; - public SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { + SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { this.seedNodes = seedNodes; this.maxConnectionsPerCluster = maxConnectionsPerCluster; this.numNodesConnected = numNodesConnected; @@ -512,18 +512,6 @@ public String modeName() { return "sniff"; } - public List getSeedNodes() { - return seedNodes; - } - - public int getMaxConnectionsPerCluster() { - return maxConnectionsPerCluster; - } - - public int getNumNodesConnected() { - return numNodesConnected; - } - @Override public RemoteConnectionStrategy.ConnectionStrategy modeType() { return RemoteConnectionStrategy.ConnectionStrategy.SNIFF; diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index e4c9eb0986b34..4d1848e5b3a98 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -755,19 +755,6 @@ public static T[] randomArray(int minArraySize, int maxArraySize, IntFunctio return array; } - public static List randomList(int maxListSize, Supplier valueConstructor) { - return randomList(0, maxListSize, valueConstructor); - } - - public static List randomList(int minListSize, int maxListSize, Supplier valueConstructor) { - final int size = randomIntBetween(minListSize, maxListSize); - List list = new ArrayList<>(); - for (int i = 0; i < size; i++) { - list.add(valueConstructor.get()); - } - return list; - } - private static final String[] TIME_SUFFIXES = new String[]{"d", "h", "ms", "s", "m", "micros", "nanos"}; From bea16009b27230f260984759e2932f1015f90b8e Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 24 Dec 2019 13:20:39 +0100 Subject: [PATCH 329/686] Add remote info to the HLRC (#50482) Unreverts the commit that added the remote info api to HLRC (#49657). The additional change to the original PR, is that `org.elasticsearch.client.cluster.RemoteConnectionInfo` now parses the initial_connect_timeout field as a string instead of a TimeValue instance. The reason that this is needed is because that the initial_connect_timeout field in the remote connection api is serialized for human consumption, but not for parsing purposes. Therefore the HLRC can't parse it correctly (which caused test failures in CI, but not in the PR CI :( ). The way this field is serialized needs to be changed in the remote connection api, but that is a breaking change. We should wait making this change until rest api versioning is introduced. Co-Authored-By: j-bean anton.shuvaev91@gmail.com --- .../elasticsearch/client/ClusterClient.java | 31 ++++ .../client/ClusterRequestConverters.java | 5 + .../client/cluster/ProxyModeInfo.java | 75 ++++++++++ .../client/cluster/RemoteConnectionInfo.java | 139 ++++++++++++++++++ .../client/cluster/RemoteInfoRequest.java | 28 ++++ .../client/cluster/RemoteInfoResponse.java | 59 ++++++++ .../client/cluster/SniffModeInfo.java | 76 ++++++++++ .../java/org/elasticsearch/client/CCRIT.java | 31 +--- .../elasticsearch/client/ClusterClientIT.java | 45 ++++++ .../client/ClusterRequestConvertersTests.java | 10 ++ .../client/ESRestHighLevelClientTestCase.java | 37 +++++ .../client/RestHighLevelClientTests.java | 10 +- .../cluster/RemoteInfoResponseTests.java | 112 ++++++++++++++ .../documentation/CCRDocumentationIT.java | 26 +--- .../ClusterClientDocumentationIT.java | 60 ++++++++ .../documentation/ILMDocumentationIT.java | 8 - .../high-level/cluster/remote_info.asciidoc | 32 ++++ .../high-level/supported-apis.asciidoc | 2 + .../cluster/remote/RemoteInfoResponse.java | 2 +- .../transport/ProxyConnectionStrategy.java | 16 +- .../transport/RemoteConnectionInfo.java | 14 +- .../transport/SniffConnectionStrategy.java | 16 +- .../org/elasticsearch/test/ESTestCase.java | 13 ++ 23 files changed, 773 insertions(+), 74 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java create mode 100644 docs/java-rest/high-level/cluster/remote_info.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java index 5e99975f51491..14fbf3e1f6d0f 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java @@ -26,6 +26,8 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; +import org.elasticsearch.client.cluster.RemoteInfoRequest; +import org.elasticsearch.client.cluster.RemoteInfoResponse; import org.elasticsearch.rest.RestStatus; import java.io.IOException; @@ -138,4 +140,33 @@ public Cancellable healthAsync(ClusterHealthRequest healthRequest, RequestOption return restHighLevelClient.performRequestAsyncAndParseEntity(healthRequest, ClusterRequestConverters::clusterHealth, options, ClusterHealthResponse::fromXContent, listener, singleton(RestStatus.REQUEST_TIMEOUT.getStatus())); } + + /** + * Get the remote cluster information using the Remote cluster info API. + * See Remote cluster info + * API on elastic.co + * @param request the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public RemoteInfoResponse remoteInfo(RemoteInfoRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, ClusterRequestConverters::remoteInfo, options, + RemoteInfoResponse::fromXContent, singleton(RestStatus.REQUEST_TIMEOUT.getStatus())); + } + + /** + * Asynchronously get remote cluster information using the Remote cluster info API. + * See Remote cluster info + * API on elastic.co + * @param request the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + * @return cancellable that may be used to cancel the request + */ + public Cancellable remoteInfoAsync(RemoteInfoRequest request, RequestOptions options, + ActionListener listener) { + return restHighLevelClient.performRequestAsyncAndParseEntity(request, ClusterRequestConverters::remoteInfo, options, + RemoteInfoResponse::fromXContent, listener, singleton(RestStatus.REQUEST_TIMEOUT.getStatus())); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java index a246402b505cc..74b2c3b7c6aae 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterRequestConverters.java @@ -25,6 +25,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.support.ActiveShardCount; +import org.elasticsearch.client.cluster.RemoteInfoRequest; import org.elasticsearch.common.Strings; import java.io.IOException; @@ -76,4 +77,8 @@ static Request clusterHealth(ClusterHealthRequest healthRequest) { request.addParameters(params.asMap()); return request; } + + static Request remoteInfo(RemoteInfoRequest remoteInfoRequest) { + return new Request(HttpGet.METHOD_NAME, "/_remote/info"); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java new file mode 100644 index 0000000000000..0fc4f240eb8ef --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java @@ -0,0 +1,75 @@ +/* + * 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. + */ + +package org.elasticsearch.client.cluster; + +import java.util.Objects; + +public class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { + static final String NAME = "proxy"; + static final String ADDRESS = "address"; + static final String NUM_SOCKETS_CONNECTED = "num_sockets_connected"; + static final String MAX_SOCKET_CONNECTIONS = "max_socket_connections"; + private final String address; + private final int maxSocketConnections; + private final int numSocketsConnected; + + ProxyModeInfo(String address, int maxSocketConnections, int numSocketsConnected) { + this.address = address; + this.maxSocketConnections = maxSocketConnections; + this.numSocketsConnected = numSocketsConnected; + } + + @Override + public boolean isConnected() { + return numSocketsConnected > 0; + } + + @Override + public String modeName() { + return NAME; + } + + public String getAddress() { + return address; + } + + public int getMaxSocketConnections() { + return maxSocketConnections; + } + + public int getNumSocketsConnected() { + return numSocketsConnected; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProxyModeInfo otherProxy = (ProxyModeInfo) o; + return maxSocketConnections == otherProxy.maxSocketConnections && + numSocketsConnected == otherProxy.numSocketsConnected && + Objects.equals(address, otherProxy.address); + } + + @Override + public int hashCode() { + return Objects.hash(address, maxSocketConnections, numSocketsConnected); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java new file mode 100644 index 0000000000000..2bf99c61085c4 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java @@ -0,0 +1,139 @@ +/* + * 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. + */ + +package org.elasticsearch.client.cluster; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * This class encapsulates all remote cluster information to be rendered on + * {@code _remote/info} requests. + */ +public final class RemoteConnectionInfo { + private static final String CONNECTED = "connected"; + private static final String MODE = "mode"; + private static final String INITIAL_CONNECT_TIMEOUT = "initial_connect_timeout"; + private static final String SKIP_UNAVAILABLE = "skip_unavailable"; + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "RemoteConnectionInfoObjectParser", + false, + (args, clusterAlias) -> { + String mode = (String) args[1]; + ModeInfo modeInfo; + if (mode.equals(ProxyModeInfo.NAME)) { + modeInfo = new ProxyModeInfo((String) args[4], (int) args[5], (int) args[6]); + } else if (mode.equals(SniffModeInfo.NAME)) { + modeInfo = new SniffModeInfo((List) args[7], (int) args[8], (int) args[9]); + } else { + throw new IllegalArgumentException("mode cannot be " + mode); + } + return new RemoteConnectionInfo(clusterAlias, + modeInfo, + (String) args[2], + (boolean) args[3]); + }); + + static { + PARSER.declareBoolean(constructorArg(), new ParseField(CONNECTED)); + PARSER.declareString(constructorArg(), new ParseField(MODE)); + PARSER.declareString(constructorArg(), new ParseField(INITIAL_CONNECT_TIMEOUT)); + PARSER.declareBoolean(constructorArg(), new ParseField(SKIP_UNAVAILABLE)); + + PARSER.declareString(optionalConstructorArg(), new ParseField(ProxyModeInfo.ADDRESS)); + PARSER.declareInt(optionalConstructorArg(), new ParseField(ProxyModeInfo.MAX_SOCKET_CONNECTIONS)); + PARSER.declareInt(optionalConstructorArg(), new ParseField(ProxyModeInfo.NUM_SOCKETS_CONNECTED)); + + PARSER.declareStringArray(optionalConstructorArg(), new ParseField(SniffModeInfo.SEEDS)); + PARSER.declareInt(optionalConstructorArg(), new ParseField(SniffModeInfo.MAX_CONNECTIONS_PER_CLUSTER)); + PARSER.declareInt(optionalConstructorArg(), new ParseField(SniffModeInfo.NUM_NODES_CONNECTED)); + } + + private final ModeInfo modeInfo; + // TODO: deprecate and remove this field in favor of initialConnectionTimeout field that is of type TimeValue. + // When rest api versioning exists then change org.elasticsearch.transport.RemoteConnectionInfo to properly serialize + // the initialConnectionTimeout field so that we can properly parse initialConnectionTimeout as TimeValue + private final String initialConnectionTimeoutString; + private final String clusterAlias; + private final boolean skipUnavailable; + + RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, String initialConnectionTimeoutString, boolean skipUnavailable) { + this.clusterAlias = clusterAlias; + this.modeInfo = modeInfo; + this.initialConnectionTimeoutString = initialConnectionTimeoutString; + this.skipUnavailable = skipUnavailable; + } + + public boolean isConnected() { + return modeInfo.isConnected(); + } + + public String getClusterAlias() { + return clusterAlias; + } + + public ModeInfo getModeInfo() { + return modeInfo; + } + + public String getInitialConnectionTimeoutString() { + return initialConnectionTimeoutString; + } + + public boolean isSkipUnavailable() { + return skipUnavailable; + } + + public static RemoteConnectionInfo fromXContent(XContentParser parser, String clusterAlias) throws IOException { + return PARSER.parse(parser, clusterAlias); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RemoteConnectionInfo that = (RemoteConnectionInfo) o; + return skipUnavailable == that.skipUnavailable && + Objects.equals(modeInfo, that.modeInfo) && + Objects.equals(initialConnectionTimeoutString, that.initialConnectionTimeoutString) && + Objects.equals(clusterAlias, that.clusterAlias); + } + + @Override + public int hashCode() { + return Objects.hash(modeInfo, initialConnectionTimeoutString, clusterAlias, skipUnavailable); + } + + public interface ModeInfo { + + boolean isConnected(); + + String modeName(); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java new file mode 100644 index 0000000000000..5ffc8afc073c6 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java @@ -0,0 +1,28 @@ +/* + * 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. + */ +package org.elasticsearch.client.cluster; + +import org.elasticsearch.client.Validatable; + +/** + * The request object used by the Remote cluster info API. + */ +public final class RemoteInfoRequest implements Validatable { + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java new file mode 100644 index 0000000000000..cc453049667b1 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java @@ -0,0 +1,59 @@ +/* + * 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. + */ +package org.elasticsearch.client.cluster; + +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * A response to _remote/info API request. + */ +public final class RemoteInfoResponse { + + private List infos; + + RemoteInfoResponse(Collection infos) { + this.infos = List.copyOf(infos); + } + + public List getInfos() { + return infos; + } + + public static RemoteInfoResponse fromXContent(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + + List infos = new ArrayList<>(); + + XContentParser.Token token; + while ((token = parser.nextToken()) == XContentParser.Token.FIELD_NAME) { + String clusterAlias = parser.currentName(); + RemoteConnectionInfo info = RemoteConnectionInfo.fromXContent(parser, clusterAlias); + infos.add(info); + } + ensureExpectedToken(XContentParser.Token.END_OBJECT, token, parser::getTokenLocation); + return new RemoteInfoResponse(infos); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java new file mode 100644 index 0000000000000..b0e75979975ee --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java @@ -0,0 +1,76 @@ +/* + * 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. + */ + +package org.elasticsearch.client.cluster; + +import java.util.List; +import java.util.Objects; + +public class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { + public static final String NAME = "sniff"; + static final String SEEDS = "seeds"; + static final String NUM_NODES_CONNECTED = "num_nodes_connected"; + static final String MAX_CONNECTIONS_PER_CLUSTER = "max_connections_per_cluster"; + final List seedNodes; + final int maxConnectionsPerCluster; + final int numNodesConnected; + + SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { + this.seedNodes = seedNodes; + this.maxConnectionsPerCluster = maxConnectionsPerCluster; + this.numNodesConnected = numNodesConnected; + } + + @Override + public boolean isConnected() { + return numNodesConnected > 0; + } + + @Override + public String modeName() { + return NAME; + } + + public List getSeedNodes() { + return seedNodes; + } + + public int getMaxConnectionsPerCluster() { + return maxConnectionsPerCluster; + } + + public int getNumNodesConnected() { + return numNodesConnected; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SniffModeInfo sniff = (SniffModeInfo) o; + return maxConnectionsPerCluster == sniff.maxConnectionsPerCluster && + numNodesConnected == sniff.numNodesConnected && + Objects.equals(seedNodes, sniff.seedNodes); + } + + @Override + public int hashCode() { + return Objects.hash(seedNodes, maxConnectionsPerCluster, numNodesConnected); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java index 6be2efe98c48b..c6678102fe2b1 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java @@ -19,10 +19,7 @@ package org.elasticsearch.client; -import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; @@ -51,9 +48,7 @@ import org.elasticsearch.client.indices.CloseIndexRequest; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.CreateIndexResponse; -import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.test.rest.yaml.ObjectPath; import org.junit.Before; @@ -74,27 +69,7 @@ public class CCRIT extends ESRestHighLevelClientTestCase { @Before public void setupRemoteClusterConfig() throws Exception { - // Configure local cluster as remote cluster: - // TODO: replace with nodes info highlevel rest client code when it is available: - final Request request = new Request("GET", "/_nodes"); - Map nodesResponse = (Map) toMap(client().performRequest(request)).get("nodes"); - // Select node info of first node (we don't know the node id): - nodesResponse = (Map) nodesResponse.get(nodesResponse.keySet().iterator().next()); - String transportAddress = (String) nodesResponse.get("transport_address"); - - ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); - updateSettingsRequest.transientSettings(Collections.singletonMap("cluster.remote.local_cluster.seeds", transportAddress)); - ClusterUpdateSettingsResponse updateSettingsResponse = - highLevelClient().cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); - assertThat(updateSettingsResponse.isAcknowledged(), is(true)); - - assertBusy(() -> { - Map localConnection = (Map) toMap(client() - .performRequest(new Request("GET", "/_remote/info"))) - .get("local_cluster"); - assertThat(localConnection, notNullValue()); - assertThat(localConnection.get("connected"), is(true)); - }); + setupRemoteClusterConfig("local_cluster"); } public void testIndexFollowing() throws Exception { @@ -311,8 +286,4 @@ public void testAutoFollowing() throws Exception { assertThat(pauseFollowResponse.isAcknowledged(), is(true)); } - private static Map toMap(Response response) throws IOException { - return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); - } - } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java index e78e7ec7ca6d3..45ba23c2a3745 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java @@ -27,19 +27,27 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; +import org.elasticsearch.client.cluster.RemoteConnectionInfo; +import org.elasticsearch.client.cluster.RemoteInfoRequest; +import org.elasticsearch.client.cluster.RemoteInfoResponse; +import org.elasticsearch.client.cluster.SniffModeInfo; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.health.ClusterIndexHealth; import org.elasticsearch.cluster.health.ClusterShardHealth; import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.indices.recovery.RecoverySettings; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.transport.RemoteClusterService; +import org.elasticsearch.transport.SniffConnectionStrategy; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import static java.util.Collections.emptyMap; @@ -297,4 +305,41 @@ public void testClusterHealthNotFoundIndex() throws IOException { assertNoIndices(response); } + public void testRemoteInfo() throws Exception { + String clusterAlias = "local_cluster"; + setupRemoteClusterConfig(clusterAlias); + + ClusterGetSettingsRequest settingsRequest = new ClusterGetSettingsRequest(); + settingsRequest.includeDefaults(true); + ClusterGetSettingsResponse settingsResponse = highLevelClient().cluster().getSettings(settingsRequest, RequestOptions.DEFAULT); + + List seeds = SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS + .getConcreteSettingForNamespace(clusterAlias) + .get(settingsResponse.getTransientSettings()); + int connectionsPerCluster = SniffConnectionStrategy.REMOTE_CONNECTIONS_PER_CLUSTER + .get(settingsResponse.getDefaultSettings()); + TimeValue initialConnectionTimeout = RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING + .get(settingsResponse.getDefaultSettings()); + boolean skipUnavailable = RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE + .getConcreteSettingForNamespace(clusterAlias) + .get(settingsResponse.getDefaultSettings()); + + RemoteInfoRequest request = new RemoteInfoRequest(); + RemoteInfoResponse response = execute(request, highLevelClient().cluster()::remoteInfo, + highLevelClient().cluster()::remoteInfoAsync); + + assertThat(response, notNullValue()); + assertThat(response.getInfos().size(), equalTo(1)); + RemoteConnectionInfo info = response.getInfos().get(0); + assertThat(info.getClusterAlias(), equalTo(clusterAlias)); + assertThat(info.getInitialConnectionTimeoutString(), equalTo(initialConnectionTimeout.toString())); + assertThat(info.isSkipUnavailable(), equalTo(skipUnavailable)); + assertThat(info.getModeInfo().modeName(), equalTo(SniffModeInfo.NAME)); + assertThat(info.getModeInfo().isConnected(), equalTo(true)); + SniffModeInfo sniffModeInfo = (SniffModeInfo) info.getModeInfo(); + assertThat(sniffModeInfo.getMaxConnectionsPerCluster(), equalTo(connectionsPerCluster)); + assertThat(sniffModeInfo.getNumNodesConnected(), equalTo(1)); + assertThat(sniffModeInfo.getSeedNodes(), equalTo(seeds)); + } + } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java index 9b7b5b0d284dc..59bf52a677321 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterRequestConvertersTests.java @@ -26,6 +26,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.master.AcknowledgedRequest; +import org.elasticsearch.client.cluster.RemoteInfoRequest; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.Priority; import org.elasticsearch.test.ESTestCase; @@ -37,6 +38,7 @@ import java.util.Locale; import java.util.Map; +import static java.util.Collections.emptyMap; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.nullValue; @@ -147,4 +149,12 @@ public void testClusterHealth() { } Assert.assertThat(request.getParameters(), equalTo(expectedParams)); } + + public void testRemoteInfo() { + RemoteInfoRequest request = new RemoteInfoRequest(); + Request expectedRequest = ClusterRequestConverters.remoteInfo(request); + assertEquals("/_remote/info", expectedRequest.getEndpoint()); + assertEquals(HttpGet.METHOD_NAME, expectedRequest.getMethod()); + assertEquals(emptyMap(), expectedRequest.getParameters()); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java index a9012fda042d9..174c98f3642df 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java @@ -19,19 +19,25 @@ package org.elasticsearch.client; +import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; import org.elasticsearch.action.ingest.PutPipelineRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.cluster.RemoteInfoRequest; +import org.elasticsearch.client.cluster.RemoteInfoResponse; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.common.Booleans; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.ingest.Pipeline; import org.elasticsearch.search.SearchHit; @@ -45,10 +51,15 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collections; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +import static java.util.Collections.singletonMap; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public abstract class ESRestHighLevelClientTestCase extends ESRestTestCase { @@ -243,4 +254,30 @@ protected void createIndexWithMultipleShards(String index) throws IOException { ); highLevelClient().indices().create(indexRequest, RequestOptions.DEFAULT); } + + protected static void setupRemoteClusterConfig(String remoteClusterName) throws Exception { + // Configure local cluster as remote cluster: + // TODO: replace with nodes info highlevel rest client code when it is available: + final Request request = new Request("GET", "/_nodes"); + Map nodesResponse = (Map) toMap(client().performRequest(request)).get("nodes"); + // Select node info of first node (we don't know the node id): + nodesResponse = (Map) nodesResponse.get(nodesResponse.keySet().iterator().next()); + String transportAddress = (String) nodesResponse.get("transport_address"); + + ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); + updateSettingsRequest.transientSettings(singletonMap("cluster.remote." + remoteClusterName + ".seeds", transportAddress)); + ClusterUpdateSettingsResponse updateSettingsResponse = + restHighLevelClient.cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); + assertThat(updateSettingsResponse.isAcknowledged(), is(true)); + + assertBusy(() -> { + RemoteInfoResponse response = highLevelClient().cluster().remoteInfo(new RemoteInfoRequest(), RequestOptions.DEFAULT); + assertThat(response, notNullValue()); + assertThat(response.getInfos().size(), greaterThan(0)); + }); + } + + protected static Map toMap(Response response) throws IOException { + return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index 4039ec251d335..b3457eedb5569 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -59,8 +59,8 @@ import org.elasticsearch.client.ml.dataframe.OutlierDetection; import org.elasticsearch.client.ml.dataframe.evaluation.classification.AccuracyMetric; import org.elasticsearch.client.ml.dataframe.evaluation.classification.Classification; -import org.elasticsearch.client.ml.dataframe.evaluation.regression.MeanSquaredErrorMetric; import org.elasticsearch.client.ml.dataframe.evaluation.classification.MulticlassConfusionMatrixMetric; +import org.elasticsearch.client.ml.dataframe.evaluation.regression.MeanSquaredErrorMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.RSquaredMetric; import org.elasticsearch.client.ml.dataframe.evaluation.regression.Regression; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.AucRocMetric; @@ -68,14 +68,14 @@ import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.ConfusionMatrixMetric; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.PrecisionMetric; import org.elasticsearch.client.ml.dataframe.evaluation.softclassification.RecallMetric; +import org.elasticsearch.client.ml.inference.preprocessing.FrequencyEncoding; +import org.elasticsearch.client.ml.inference.preprocessing.OneHotEncoding; +import org.elasticsearch.client.ml.inference.preprocessing.TargetMeanEncoding; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.Ensemble; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.LogisticRegression; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.WeightedMode; import org.elasticsearch.client.ml.inference.trainedmodel.ensemble.WeightedSum; import org.elasticsearch.client.ml.inference.trainedmodel.tree.Tree; -import org.elasticsearch.client.ml.inference.preprocessing.FrequencyEncoding; -import org.elasticsearch.client.ml.inference.preprocessing.OneHotEncoding; -import org.elasticsearch.client.ml.inference.preprocessing.TargetMeanEncoding; import org.elasticsearch.client.transform.transforms.SyncConfig; import org.elasticsearch.client.transform.transforms.TimeSyncConfig; import org.elasticsearch.common.CheckedFunction; @@ -106,7 +106,6 @@ import org.elasticsearch.test.InternalAggregationTestCase; import org.elasticsearch.test.rest.yaml.restspec.ClientYamlSuiteRestApi; import org.elasticsearch.test.rest.yaml.restspec.ClientYamlSuiteRestSpec; - import org.hamcrest.Matchers; import org.junit.Before; @@ -773,7 +772,6 @@ public void testProvidedNamedXContents() { public void testApiNamingConventions() throws Exception { //this list should be empty once the high-level client is feature complete String[] notYetSupportedApi = new String[]{ - "cluster.remote_info", "create", "get_script_context", "get_script_languages", diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java new file mode 100644 index 0000000000000..88f8f6f533e1d --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/cluster/RemoteInfoResponseTests.java @@ -0,0 +1,112 @@ +/* + * 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. + */ + +package org.elasticsearch.client.cluster; + +import org.elasticsearch.client.AbstractResponseTestCase; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.transport.ProxyConnectionStrategy; +import org.elasticsearch.transport.RemoteConnectionInfo; +import org.elasticsearch.transport.SniffConnectionStrategy; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static org.hamcrest.Matchers.equalTo; + +public class RemoteInfoResponseTests extends AbstractResponseTestCase { + + @Override + protected org.elasticsearch.action.admin.cluster.remote.RemoteInfoResponse createServerTestInstance(XContentType xContentType) { + int numRemoteInfos = randomIntBetween(0, 8); + List remoteInfos = new ArrayList<>(); + for (int i = 0; i < numRemoteInfos; i++) { + remoteInfos.add(createRandomRemoteConnectionInfo()); + } + return new org.elasticsearch.action.admin.cluster.remote.RemoteInfoResponse(remoteInfos); + } + + @Override + protected RemoteInfoResponse doParseToClientInstance(XContentParser parser) throws IOException { + return RemoteInfoResponse.fromXContent(parser); + } + + @Override + protected void assertInstances(org.elasticsearch.action.admin.cluster.remote.RemoteInfoResponse serverTestInstance, + RemoteInfoResponse clientInstance) { + assertThat(clientInstance.getInfos().size(), equalTo(serverTestInstance.getInfos().size())); + Map serverInfos = serverTestInstance.getInfos().stream() + .collect(toMap(RemoteConnectionInfo::getClusterAlias, identity())); + for (org.elasticsearch.client.cluster.RemoteConnectionInfo clientRemoteInfo : clientInstance.getInfos()) { + RemoteConnectionInfo serverRemoteInfo = serverInfos.get(clientRemoteInfo.getClusterAlias()); + assertThat(clientRemoteInfo.getClusterAlias(), equalTo(serverRemoteInfo.getClusterAlias())); + assertThat(clientRemoteInfo.getInitialConnectionTimeoutString(), + equalTo(serverRemoteInfo.getInitialConnectionTimeout().toString())); + assertThat(clientRemoteInfo.isConnected(), equalTo(serverRemoteInfo.isConnected())); + assertThat(clientRemoteInfo.isSkipUnavailable(), equalTo(serverRemoteInfo.isSkipUnavailable())); + assertThat(clientRemoteInfo.getModeInfo().isConnected(), equalTo(serverRemoteInfo.getModeInfo().isConnected())); + assertThat(clientRemoteInfo.getModeInfo().modeName(), equalTo(serverRemoteInfo.getModeInfo().modeName())); + if (clientRemoteInfo.getModeInfo().modeName().equals(SniffModeInfo.NAME)) { + SniffModeInfo clientModeInfo = + (SniffModeInfo) clientRemoteInfo.getModeInfo(); + SniffConnectionStrategy.SniffModeInfo serverModeInfo = + (SniffConnectionStrategy.SniffModeInfo) serverRemoteInfo.getModeInfo(); + assertThat(clientModeInfo.getMaxConnectionsPerCluster(), equalTo(serverModeInfo.getMaxConnectionsPerCluster())); + assertThat(clientModeInfo.getNumNodesConnected(), equalTo(serverModeInfo.getNumNodesConnected())); + assertThat(clientModeInfo.getSeedNodes(), equalTo(serverModeInfo.getSeedNodes())); + } else if (clientRemoteInfo.getModeInfo().modeName().equals(ProxyModeInfo.NAME)) { + ProxyModeInfo clientModeInfo = + (ProxyModeInfo) clientRemoteInfo.getModeInfo(); + ProxyConnectionStrategy.ProxyModeInfo serverModeInfo = + (ProxyConnectionStrategy.ProxyModeInfo) serverRemoteInfo.getModeInfo(); + assertThat(clientModeInfo.getAddress(), equalTo(serverModeInfo.getAddress())); + assertThat(clientModeInfo.getMaxSocketConnections(), equalTo(serverModeInfo.getMaxSocketConnections())); + assertThat(clientModeInfo.getNumSocketsConnected(), equalTo(serverModeInfo.getNumSocketsConnected())); + } else { + fail("impossible case"); + } + } + } + + private static RemoteConnectionInfo createRandomRemoteConnectionInfo() { + RemoteConnectionInfo.ModeInfo modeInfo; + if (randomBoolean()) { + String address = randomAlphaOfLength(8); + int maxSocketConnections = randomInt(5); + int numSocketsConnected = randomInt(5); + modeInfo = new ProxyConnectionStrategy.ProxyModeInfo(address, maxSocketConnections, numSocketsConnected); + } else { + List seedNodes = randomList(randomInt(8), () -> randomAlphaOfLength(8)); + int maxConnectionsPerCluster = randomInt(5); + int numNodesConnected = randomInt(5); + modeInfo = new SniffConnectionStrategy.SniffModeInfo(seedNodes, maxConnectionsPerCluster, numNodesConnected); + } + String clusterAlias = randomAlphaOfLength(8); + TimeValue initialConnectionTimeout = TimeValue.parseTimeValue(randomTimeValue(), "randomInitialConnectionTimeout"); + boolean skipUnavailable = randomBoolean(); + return new RemoteConnectionInfo(clusterAlias, modeInfo, initialConnectionTimeout, skipUnavailable); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java index 48f7adda8445c..5e7da8142129d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java @@ -19,11 +19,8 @@ package org.elasticsearch.client.documentation; -import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.client.ESRestHighLevelClientTestCase; @@ -57,8 +54,6 @@ import org.elasticsearch.client.indices.CloseIndexRequest; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.CreateIndexResponse; -import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.rest.yaml.ObjectPath; import org.junit.Before; @@ -76,21 +71,8 @@ public class CCRDocumentationIT extends ESRestHighLevelClientTestCase { @Before - public void setupRemoteClusterConfig() throws IOException { - RestHighLevelClient client = highLevelClient(); - // Configure local cluster as remote cluster: - // TODO: replace with nodes info highlevel rest client code when it is available: - final Request request = new Request("GET", "/_nodes"); - Map nodesResponse = (Map) toMap(client().performRequest(request)).get("nodes"); - // Select node info of first node (we don't know the node id): - nodesResponse = (Map) nodesResponse.get(nodesResponse.keySet().iterator().next()); - String transportAddress = (String) nodesResponse.get("transport_address"); - - ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); - updateSettingsRequest.transientSettings(Collections.singletonMap("cluster.remote.local.seeds", transportAddress)); - ClusterUpdateSettingsResponse updateSettingsResponse = - client.cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); - assertThat(updateSettingsResponse.isAcknowledged(), is(true)); + public void setupRemoteClusterConfig() throws Exception { + setupRemoteClusterConfig("local"); } public void testPutFollow() throws Exception { @@ -985,8 +967,4 @@ public void onFailure(Exception e) { } } - static Map toMap(Response response) throws IOException { - return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); - } - } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java index 8b7f1577114b0..5cc7a4424fcaa 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java @@ -31,6 +31,9 @@ import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.cluster.RemoteConnectionInfo; +import org.elasticsearch.client.cluster.RemoteInfoRequest; +import org.elasticsearch.client.cluster.RemoteInfoResponse; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.health.ClusterIndexHealth; @@ -46,6 +49,7 @@ import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -415,4 +419,60 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } + + public void testRemoteInfo() throws Exception { + setupRemoteClusterConfig("local_cluster"); + + RestHighLevelClient client = highLevelClient(); + + // tag::remote-info-request + RemoteInfoRequest request = new RemoteInfoRequest(); + // end::remote-info-request + + // tag::remote-info-execute + RemoteInfoResponse response = client.cluster().remoteInfo(request, RequestOptions.DEFAULT); // <1> + // end::remote-info-execute + + // tag::remote-info-response + List infos = response.getInfos(); + // end::remote-info-response + + assertThat(infos.size(), greaterThan(0)); + } + + public void testRemoteInfoAsync() throws Exception { + setupRemoteClusterConfig("local_cluster"); + + RestHighLevelClient client = highLevelClient(); + + // tag::remote-info-request + RemoteInfoRequest request = new RemoteInfoRequest(); + // end::remote-info-request + + + // tag::remote-info-execute-listener + ActionListener listener = + new ActionListener<>() { + @Override + public void onResponse(RemoteInfoResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::remote-info-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::health-execute-async + client.cluster().remoteInfoAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::health-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java index a9852c405a49d..a458c3ec2551d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java @@ -19,7 +19,6 @@ package org.elasticsearch.client.documentation; -import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; @@ -28,7 +27,6 @@ import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.RequestOptions; -import org.elasticsearch.client.Response; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.core.AcknowledgedResponse; import org.elasticsearch.client.ilm.DeleteAction; @@ -78,8 +76,6 @@ import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.snapshots.SnapshotState; @@ -1210,8 +1206,4 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } - static Map toMap(Response response) throws IOException { - return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); - } - } diff --git a/docs/java-rest/high-level/cluster/remote_info.asciidoc b/docs/java-rest/high-level/cluster/remote_info.asciidoc new file mode 100644 index 0000000000000..6496a04a3a76c --- /dev/null +++ b/docs/java-rest/high-level/cluster/remote_info.asciidoc @@ -0,0 +1,32 @@ +-- +:api: remote-info +:request: RemoteInfoRequest +:response: RemoteInfoResponse +-- + +[id="{upid}-{api}"] +=== Remote Cluster Info API + +The Remote cluster info API allows to get all of the configured remote cluster information. + +[id="{upid}-{api}-request"] +==== Remote Cluster Info Request + +A +{request}+: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- + +There are no required parameters. + +==== Remote Cluster Info Response + +The returned +{response}+ allows to retrieve remote cluster information. +It returns connection and endpoint information keyed by the configured remote cluster alias. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 2191e795ebbb1..e0d228b5d1e4b 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -168,12 +168,14 @@ The Java High Level REST Client supports the following Cluster APIs: * <> * <> * <> +* <> :upid: {mainid}-cluster :doc-tests-file: {doc-tests}/ClusterClientDocumentationIT.java include::cluster/put_settings.asciidoc[] include::cluster/get_settings.asciidoc[] include::cluster/health.asciidoc[] +include::cluster/remote_info.asciidoc[] == Ingest APIs The Java High Level REST Client supports the following Ingest APIs: diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java index 894cd9cf9fec4..9d72da7746974 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/remote/RemoteInfoResponse.java @@ -39,7 +39,7 @@ public final class RemoteInfoResponse extends ActionResponse implements ToXConte infos = in.readList(RemoteConnectionInfo::new); } - RemoteInfoResponse(Collection infos) { + public RemoteInfoResponse(Collection infos) { this.infos = List.copyOf(infos); } diff --git a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java index 78c9f5f28154d..14ca217ade1b8 100644 --- a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java @@ -268,13 +268,13 @@ private static TransportAddress resolveAddress(String address) { return new TransportAddress(parseConfiguredAddress(address)); } - static class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { + public static class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { private final String address; private final int maxSocketConnections; private final int numSocketsConnected; - ProxyModeInfo(String address, int maxSocketConnections, int numSocketsConnected) { + public ProxyModeInfo(String address, int maxSocketConnections, int numSocketsConnected) { this.address = address; this.maxSocketConnections = maxSocketConnections; this.numSocketsConnected = numSocketsConnected; @@ -311,6 +311,18 @@ public String modeName() { return "proxy"; } + public String getAddress() { + return address; + } + + public int getMaxSocketConnections() { + return maxSocketConnections; + } + + public int getNumSocketsConnected() { + return numSocketsConnected; + } + @Override public RemoteConnectionStrategy.ConnectionStrategy modeType() { return RemoteConnectionStrategy.ConnectionStrategy.PROXY; diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java index e721a0b617fd1..152cafccb61e0 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java @@ -43,7 +43,7 @@ public final class RemoteConnectionInfo implements ToXContentFragment, Writeable final String clusterAlias; final boolean skipUnavailable; - RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, TimeValue initialConnectionTimeout, boolean skipUnavailable) { + public RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, TimeValue initialConnectionTimeout, boolean skipUnavailable) { this.clusterAlias = clusterAlias; this.modeInfo = modeInfo; this.initialConnectionTimeout = initialConnectionTimeout; @@ -77,6 +77,18 @@ public String getClusterAlias() { return clusterAlias; } + public ModeInfo getModeInfo() { + return modeInfo; + } + + public TimeValue getInitialConnectionTimeout() { + return initialConnectionTimeout; + } + + public boolean isSkipUnavailable() { + return skipUnavailable; + } + @Override public void writeTo(StreamOutput out) throws IOException { // TODO: Change to 7.6 after backport diff --git a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java index 97b5e318841d6..fcc4bde951dd0 100644 --- a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java @@ -465,13 +465,13 @@ private boolean proxyChanged(String oldProxy, String newProxy) { return Objects.equals(oldProxy, newProxy) == false; } - static class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { + public static class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { final List seedNodes; final int maxConnectionsPerCluster; final int numNodesConnected; - SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { + public SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { this.seedNodes = seedNodes; this.maxConnectionsPerCluster = maxConnectionsPerCluster; this.numNodesConnected = numNodesConnected; @@ -512,6 +512,18 @@ public String modeName() { return "sniff"; } + public List getSeedNodes() { + return seedNodes; + } + + public int getMaxConnectionsPerCluster() { + return maxConnectionsPerCluster; + } + + public int getNumNodesConnected() { + return numNodesConnected; + } + @Override public RemoteConnectionStrategy.ConnectionStrategy modeType() { return RemoteConnectionStrategy.ConnectionStrategy.SNIFF; diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index 4d1848e5b3a98..e4c9eb0986b34 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -755,6 +755,19 @@ public static T[] randomArray(int minArraySize, int maxArraySize, IntFunctio return array; } + public static List randomList(int maxListSize, Supplier valueConstructor) { + return randomList(0, maxListSize, valueConstructor); + } + + public static List randomList(int minListSize, int maxListSize, Supplier valueConstructor) { + final int size = randomIntBetween(minListSize, maxListSize); + List list = new ArrayList<>(); + for (int i = 0; i < size; i++) { + list.add(valueConstructor.get()); + } + return list; + } + private static final String[] TIME_SUFFIXES = new String[]{"d", "h", "ms", "s", "m", "micros", "nanos"}; From 392d5e4f04bdceb08cd2c3b19b29c52bdaf96c39 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 24 Dec 2019 14:37:25 +0100 Subject: [PATCH 330/686] Adjusted test to use transient settings instead of default settings. --- .../test/java/org/elasticsearch/client/ClusterClientIT.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java index 45ba23c2a3745..09c8549d7258c 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java @@ -317,12 +317,12 @@ public void testRemoteInfo() throws Exception { .getConcreteSettingForNamespace(clusterAlias) .get(settingsResponse.getTransientSettings()); int connectionsPerCluster = SniffConnectionStrategy.REMOTE_CONNECTIONS_PER_CLUSTER - .get(settingsResponse.getDefaultSettings()); + .get(settingsResponse.getTransientSettings()); TimeValue initialConnectionTimeout = RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING - .get(settingsResponse.getDefaultSettings()); + .get(settingsResponse.getTransientSettings()); boolean skipUnavailable = RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE .getConcreteSettingForNamespace(clusterAlias) - .get(settingsResponse.getDefaultSettings()); + .get(settingsResponse.getTransientSettings()); RemoteInfoRequest request = new RemoteInfoRequest(); RemoteInfoResponse response = execute(request, highLevelClient().cluster()::remoteInfo, From 96c6f30a7b0f18b6e816c3ea517881755da74240 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 24 Dec 2019 08:32:07 -0500 Subject: [PATCH 331/686] Adjust BWC for peer recovery retention leases (#50351) Relates #50351 --- .../upgrades/FullClusterRestartIT.java | 6 ++-- .../elasticsearch/upgrades/RecoveryIT.java | 28 ++--------------- .../index/seqno/ReplicationTracker.java | 7 ++--- .../test/rest/ESRestTestCase.java | 31 +++++++++++++++++-- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java index 67d3007e9af16..21ae48e9c77d4 100644 --- a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java +++ b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java @@ -1278,7 +1278,7 @@ public void testOperationBasedRecovery() throws Exception { } } flush(index, true); - ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index, false); + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index); // less than 10% of the committed docs (see IndexSetting#FILE_BASED_RECOVERY_THRESHOLD_SETTING). int uncommittedDocs = randomIntBetween(0, (int) (committedDocs * 0.1)); for (int i = 0; i < uncommittedDocs; i++) { @@ -1288,7 +1288,7 @@ public void testOperationBasedRecovery() throws Exception { } else { ensureGreen(index); assertNoFileBasedRecovery(index, n -> true); - ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index, true); + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index); } } @@ -1313,7 +1313,7 @@ public void testTurnOffTranslogRetentionAfterUpgraded() throws Exception { ensureGreen(index); flush(index, true); assertEmptyTranslog(index); - ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index, true); + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index); } } } diff --git a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java index 429687853e897..080b15268db1d 100644 --- a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java +++ b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java @@ -511,28 +511,6 @@ private static Version indexVersionCreated(final String indexName) throws IOExce return Version.fromId(Integer.parseInt(ObjectPath.createFromResponse(response).evaluate(versionCreatedSetting))); } - /** - * Returns the minimum node version among all nodes of the cluster - */ - private static Version minimumNodeVersion() throws IOException { - final Request request = new Request("GET", "_nodes"); - request.addParameter("filter_path", "nodes.*.version"); - - final Response response = client().performRequest(request); - final Map nodes = ObjectPath.createFromResponse(response).evaluate("nodes"); - - Version minVersion = null; - for (Map.Entry node : nodes.entrySet()) { - @SuppressWarnings("unchecked") - Version nodeVersion = Version.fromString((String) ((Map) node.getValue()).get("version")); - if (minVersion == null || minVersion.after(nodeVersion)) { - minVersion = nodeVersion; - } - } - assertNotNull(minVersion); - return minVersion; - } - /** * Asserts that an index is closed in the cluster state. If `checkRoutingTable` is true, it also asserts * that the index has started shards. @@ -695,7 +673,7 @@ public void testOperationBasedRecovery() throws Exception { ensureGreen(index); indexDocs(index, 0, randomIntBetween(100, 200)); flush(index, randomBoolean()); - ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index, false); + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index); // uncommitted docs must be less than 10% of committed docs (see IndexSetting#FILE_BASED_RECOVERY_THRESHOLD_SETTING). indexDocs(index, randomIntBetween(0, 100), randomIntBetween(0, 3)); } else { @@ -705,9 +683,7 @@ public void testOperationBasedRecovery() throws Exception { || nodeName.startsWith(CLUSTER_NAME + "-0") || (nodeName.startsWith(CLUSTER_NAME + "-1") && Booleans.parseBoolean(System.getProperty("tests.first_round")) == false)); indexDocs(index, randomIntBetween(0, 100), randomIntBetween(0, 3)); - if (CLUSTER_TYPE == ClusterType.UPGRADED) { - ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index, true); - } + ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index); } } diff --git a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java index bfe89a65d89df..aff85f10e4bf8 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java @@ -895,11 +895,10 @@ public ReplicationTracker( this.pendingInSync = new HashSet<>(); this.routingTable = null; this.replicationGroup = null; - this.hasAllPeerRecoveryRetentionLeases = indexSettings.getIndexVersionCreated().onOrAfter(Version.V_8_0_0) + this.hasAllPeerRecoveryRetentionLeases = indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_6_0) || (indexSettings.isSoftDeleteEnabled() && - (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_6_0) || - (indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_4_0) && - indexSettings.getIndexMetaData().getState() == IndexMetaData.State.OPEN))); + indexSettings.getIndexVersionCreated().onOrAfter(Version.V_7_4_0) && + indexSettings.getIndexMetaData().getState() == IndexMetaData.State.OPEN); this.fileBasedRecoveryThreshold = IndexSettings.FILE_BASED_RECOVERY_THRESHOLD_SETTING.get(indexSettings.getSettings()); this.safeCommitInfoSupplier = safeCommitInfoSupplier; diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index f10f444e918ed..d5da4320ab3ee 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -56,6 +56,7 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.snapshots.SnapshotState; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.rest.yaml.ObjectPath; import org.hamcrest.Matchers; import org.junit.After; import org.junit.AfterClass; @@ -1132,7 +1133,8 @@ public void assertEmptyTranslog(String index) throws Exception { * Peer recovery retention leases are renewed and synced to replicas periodically (every 30 seconds). This ensures * that we have renewed every PRRL to the global checkpoint of the corresponding copy and properly synced to all copies. */ - public void ensurePeerRecoveryRetentionLeasesRenewedAndSynced(String index, boolean alwaysExists) throws Exception { + public void ensurePeerRecoveryRetentionLeasesRenewedAndSynced(String index) throws Exception { + boolean mustHavePRRLs = minimumNodeVersion().onOrAfter(Version.V_7_6_0); assertBusy(() -> { Map stats = entityAsMap(client().performRequest(new Request("GET", index + "/_stats?level=shards"))); @SuppressWarnings("unchecked") Map>> shards = @@ -1140,10 +1142,11 @@ public void ensurePeerRecoveryRetentionLeasesRenewedAndSynced(String index, bool for (List> shard : shards.values()) { for (Map copy : shard) { Integer globalCheckpoint = (Integer) XContentMapValues.extractValue("seq_no.global_checkpoint", copy); + assertThat(XContentMapValues.extractValue("seq_no.max_seq_no", copy), equalTo(globalCheckpoint)); assertNotNull(globalCheckpoint); @SuppressWarnings("unchecked") List> retentionLeases = (List>) XContentMapValues.extractValue("retention_leases.leases", copy); - if (alwaysExists == false && retentionLeases == null) { + if (mustHavePRRLs == false && retentionLeases == null) { continue; } assertNotNull(retentionLeases); @@ -1152,7 +1155,7 @@ public void ensurePeerRecoveryRetentionLeasesRenewedAndSynced(String index, bool assertThat(retentionLease.get("retaining_seq_no"), equalTo(globalCheckpoint + 1)); } } - if (alwaysExists) { + if (mustHavePRRLs) { List existingLeaseIds = retentionLeases.stream().map(lease -> (String) lease.get("id")) .collect(Collectors.toList()); List expectedLeaseIds = shard.stream() @@ -1165,4 +1168,26 @@ public void ensurePeerRecoveryRetentionLeasesRenewedAndSynced(String index, bool } }, 60, TimeUnit.SECONDS); } + + /** + * Returns the minimum node version among all nodes of the cluster + */ + protected static Version minimumNodeVersion() throws IOException { + final Request request = new Request("GET", "_nodes"); + request.addParameter("filter_path", "nodes.*.version"); + + final Response response = client().performRequest(request); + final Map nodes = ObjectPath.createFromResponse(response).evaluate("nodes"); + + Version minVersion = null; + for (Map.Entry node : nodes.entrySet()) { + @SuppressWarnings("unchecked") + Version nodeVersion = Version.fromString((String) ((Map) node.getValue()).get("version")); + if (minVersion == null || minVersion.after(nodeVersion)) { + minVersion = nodeVersion; + } + } + assertNotNull(minVersion); + return minVersion; + } } From 68668e073000411b739ad3a76396b35e54851c45 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 24 Dec 2019 15:39:56 +0100 Subject: [PATCH 332/686] Check watch count after stopping watcher in test teardown. Also disabled slm in smoke test watcher qa test. Relates to #41172 --- x-pack/qa/smoke-test-watcher/build.gradle | 3 ++- .../java/org/elasticsearch/smoketest/WatcherRestIT.java | 8 ++++++++ .../test/mustache/50_webhook_url_escaping.yml | 8 -------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/x-pack/qa/smoke-test-watcher/build.gradle b/x-pack/qa/smoke-test-watcher/build.gradle index 7bc68fee9f499..b3b638e938343 100644 --- a/x-pack/qa/smoke-test-watcher/build.gradle +++ b/x-pack/qa/smoke-test-watcher/build.gradle @@ -8,10 +8,11 @@ dependencies { testClusters.integTest { testDistribution = 'DEFAULT' + setting 'xpack.slm.enabled', 'false' setting 'xpack.ilm.enabled', 'false' setting 'xpack.security.enabled', 'false' setting 'xpack.monitoring.enabled', 'false' setting 'xpack.ml.enabled', 'false' setting 'xpack.license.self_generated.type', 'trial' setting 'logger.org.elasticsearch.xpack.watcher', 'DEBUG' -} \ No newline at end of file +} diff --git a/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/WatcherRestIT.java b/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/WatcherRestIT.java index 63efe7ad781c8..a5068f39486db 100644 --- a/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/WatcherRestIT.java +++ b/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/WatcherRestIT.java @@ -7,6 +7,7 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.elasticsearch.client.Request; import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; import org.elasticsearch.test.rest.yaml.ClientYamlTestResponse; import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; @@ -19,6 +20,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; /** Runs rest tests against external cluster */ @@ -70,6 +72,10 @@ public void startWatcher() throws Exception { @After public void stopWatcher() throws Exception { + Request deleteWatchesIndexRequest = new Request("DELETE", "/.watches"); + deleteWatchesIndexRequest.addParameter("ignore_unavailable", "true"); + adminClient().performRequest(deleteWatchesIndexRequest); + assertBusy(() -> { ClientYamlTestResponse response = getAdminExecutionContext().callApi("watcher.stats", emptyMap(), emptyList(), emptyMap()); @@ -77,6 +83,8 @@ public void stopWatcher() throws Exception { switch (state) { case "stopped": + int watcherCount = (int) response.evaluate("stats.0.watch_count"); + assertThat(watcherCount, equalTo(0)); // all good here, we are done break; case "stopping": diff --git a/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/50_webhook_url_escaping.yml b/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/50_webhook_url_escaping.yml index 88986a5546c7c..0ed3cfe04480f 100644 --- a/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/50_webhook_url_escaping.yml +++ b/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/50_webhook_url_escaping.yml @@ -1,9 +1,5 @@ --- "Test url escaping with url mustache function": - - do: - cluster.health: - wait_for_status: yellow - - do: index: index: @@ -11,10 +7,6 @@ refresh: true body: { foo: bar } - - do: {watcher.stats:{}} - - match: { "stats.0.watcher_state": "started" } - - match: { "stats.0.watch_count": 0 } - # extract http host and port from master node - do: cluster.state: {} From 3d71812b12d6ccecaac2cc694c8815726474b868 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 24 Dec 2019 08:34:03 -0800 Subject: [PATCH 333/686] [DOCS] Remove redundant results from ML APIs (#50477) --- .../apis/delete-calendar-job.asciidoc | 6 +- .../apis/delete-forecast.asciidoc | 10 +- .../apis/get-bucket.asciidoc | 5 +- .../apis/get-calendar-event.asciidoc | 17 +- .../apis/get-calendar.asciidoc | 21 +- .../apis/get-category.asciidoc | 8 +- .../apis/get-datafeed-stats.asciidoc | 22 +- .../apis/get-datafeed.asciidoc | 55 +--- .../apis/get-filter.asciidoc | 24 +- .../apis/get-influencer.asciidoc | 5 +- .../apis/get-job-stats.asciidoc | 96 +++---- .../anomaly-detection/apis/get-job.asciidoc | 64 +---- .../apis/get-record.asciidoc | 5 +- .../apis/post-calendar-event.asciidoc | 8 +- .../apis/put-datafeed.asciidoc | 5 +- .../anomaly-detection/apis/put-job.asciidoc | 139 ++++++++++ .../apis/update-job.asciidoc | 71 +++++- .../apis/validate-detector.asciidoc | 75 +++++- docs/reference/ml/ml-shared.asciidoc | 241 +++++------------- 19 files changed, 459 insertions(+), 418 deletions(-) diff --git a/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc index dfe56c9388d84..6b705b8c6f932 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc @@ -24,11 +24,11 @@ Deletes {anomaly-jobs} from a calendar. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the calendar. +(Required, string) Identifier for the calendar. ``:: - (Required, string) An identifier for the {anomaly-jobs}. It can be a job - identifier, a group name, or a comma-separated list of jobs or groups. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-list] [[ml-delete-calendar-job-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc index e269d532b0c27..80eb03c75b59d 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc @@ -41,12 +41,14 @@ For more information, see ==== {api-path-parms-title} ``:: - (Optional, string) A comma-separated list of forecast identifiers. - If you do not specify this optional parameter or if you specify `_all`, the - API deletes all forecasts from the job. +(Optional, string) A comma-separated list of forecast identifiers. If you do not +specify this optional parameter or if you specify `_all`, the API deletes all +forecasts from the job. ``:: - (Required, string) Required. Identifier for the job. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + [[ml-delete-forecast-query-parms]] ==== {api-query-parms-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc index f06632bcbd54f..a34d6bc714a99 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc @@ -63,10 +63,9 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=exclude-interim-results] `expand`:: (Optional, boolean) If true, the output includes anomaly records. -`page`:: -`page`.`from`::: +`page`.`from`:: (Optional, integer) Skips the specified number of buckets. -`page`.`size`::: +`page`.`size`:: (Optional, integer) Specifies the maximum number of buckets to obtain. `sort`:: diff --git a/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc index 7824c08e1da41..f3fcd65aab210 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc @@ -56,27 +56,24 @@ For more information, see [[ml-get-calendar-event-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns an array of scheduled event resources, which have the +following properties: -`events`:: - (array) An array of scheduled event resources. An events resource has the - following properties: - - `calendar_id`::: +`calendar_id`:: (string) An identifier for the calendar that contains the scheduled event. - `description`::: +`description`:: (string) A description of the scheduled event. - `end_time`::: +`end_time`:: (date) The timestamp for the end of the scheduled event in milliseconds since the epoch or ISO 8601 format. - `event_id`::: +`event_id`:: (string) An automatically-generated identifier for the scheduled event. - `start_time`::: +`start_time`:: (date) The timestamp for the beginning of the scheduled event in milliseconds since the epoch or ISO 8601 format. diff --git a/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc index f2243825b81f0..0d0d85090df69 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc @@ -40,27 +40,24 @@ For more information, see [[ml-get-calendar-request-body]] ==== {api-request-body-title} -`page`:: -`from`::: +`page`.`from`:: (Optional, integer) Skips the specified number of calendars. -`size`::: +`page`.`size`:: (Optional, integer) Specifies the maximum number of calendars to obtain. [[ml-get-calendar-results]] ==== {api-response-body-title} -The API returns the following information: +The API returns an array of calendar resources, which have the following +properties: -`calendars`:: - (array) An array of calendar resources. A calendar resource has the following - properties: - `calendar_id`::: - (string) A numerical character string that uniquely identifies the calendar. +`calendar_id`:: +(string) A numerical character string that uniquely identifies the calendar. - `job_ids`::: - (array) An array of {anomaly-job} identifiers. For example: - `["total-requests"]`. +`job_ids`:: +(array) An array of {anomaly-job} identifiers. For example: +`["total-requests"]`. [[ml-get-calendar-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc index 97acffbc4a198..66b71558e432f 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-category.asciidoc @@ -53,10 +53,9 @@ parameter, the API returns information about all categories in the {anomaly-job} [[ml-get-category-request-body]] ==== {api-request-body-title} -`page`:: -`page`.`from`::: +`page`.`from`:: (Optional, integer) Skips the specified number of categories. -`page`.`size`::: +`page`.`size`:: (Optional, integer) Specifies the maximum number of categories to obtain. [[ml-get-category-results]] @@ -72,7 +71,8 @@ The API returns an array of category objects, which have the following propertie `grok_pattern`:: experimental[] (string) A Grok pattern that could be used in {ls} or an ingest -pipeline to extract fields from messages that match the category. This field is experimental and may be changed or removed in a future release. The Grok +pipeline to extract fields from messages that match the category. This field is +experimental and may be changed or removed in a future release. The Grok patterns that are found are not optimal, but are often a good starting point for manual tweaking. diff --git a/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc index feccd52364f48..695c16761a623 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc @@ -47,8 +47,11 @@ IMPORTANT: This API returns a maximum of 10,000 {dfeeds}. ``:: (Optional, string) include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id-wildcard] ++ +-- If you do not specify one of these options, the API returns information about all {dfeeds}. +-- [[ml-get-datafeed-stats-query-parms]] ==== {api-query-parms-title} @@ -72,13 +75,14 @@ informational; you cannot update their values. include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id] `node`:: -(object) For started {dfeeds} only, the node upon which the {dfeed} is started. The {dfeed} and job will be on the same node. -`id`::: The unique identifier of the node. For example, "0-o0tOoRTwKFZifatTWKNw". -`name`::: The node name. For example, `0-o0tOo`. -`ephemeral_id`::: The node ephemeral ID. -`transport_address`::: The host and port where transport HTTP connections are +(object) For started {dfeeds} only, the node upon which the {dfeed} is started. +The {dfeed} and job will be on the same node. +`node`.`id`::: The unique identifier of the node. For example, "0-o0tOoRTwKFZifatTWKNw". +`node`.`name`::: The node name. For example, `0-o0tOo`. +`node`.`ephemeral_id`::: The node ephemeral ID. +`node`.`transport_address`::: The host and port where transport HTTP connections are accepted. For example, `127.0.0.1:9300`. -`attributes`::: For example, `{"ml.machine_memory": "17179869184"}`. +`node`.`attributes`::: For example, `{"ml.machine_memory": "17179869184"}`. `state`:: (string) The status of the {dfeed}, which can be one of the following values: @@ -95,10 +99,10 @@ this {dfeed}. //average_search_time_per_bucket_ms //bucket_count //exponential_average_search_time_per_hour_ms -`job_id`::: +`timing_stats`.`job_id`::: include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] -`search_count`::: Number of searches performed by this {dfeed}. -`total_search_time_ms`::: Total time the {dfeed} spent searching in milliseconds. +`timing_stats`.`search_count`::: Number of searches performed by this {dfeed}. +`timing_stats`.`total_search_time_ms`::: Total time the {dfeed} spent searching in milliseconds. [[ml-get-datafeed-stats-response-codes]] diff --git a/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc index 11aca1edd95e1..368824aea6d51 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc @@ -44,8 +44,11 @@ IMPORTANT: This API returns a maximum of 10,000 {dfeeds}. ``:: (Optional, string) include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id-wildcard] ++ +-- If you do not specify one of these options, the API returns information about all {dfeeds}. +-- [[ml-get-datafeed-query-parms]] ==== {api-query-parms-title} @@ -57,56 +60,8 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-datafeeds] [[ml-get-datafeed-results]] ==== {api-response-body-title} -The API returns an array of {dfeed} resources, which have the following -properties: - -`aggregations`:: -(object) -include::{docdir}/ml/ml-shared.asciidoc[tag=aggregations] - -`chunking_config`:: -(object) -include::{docdir}/ml/ml-shared.asciidoc[tag=chunking-config] - -`datafeed_id`:: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id] - -`delayed_data_check_config`:: -(object) -include::{docdir}/ml/ml-shared.asciidoc[tag=delayed-data-check-config] - -`frequency`:: -(<>) -include::{docdir}/ml/ml-shared.asciidoc[tag=frequency] - -`indices`:: -(array) -include::{docdir}/ml/ml-shared.asciidoc[tag=indices] - -`job_id`:: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-datafeed] - -`max_empty_searches`:: -(integer) -include::{docdir}/ml/ml-shared.asciidoc[tag=max-empty-searches] - -`query`:: -(object) -include::{docdir}/ml/ml-shared.asciidoc[tag=query] - -`query_delay`:: -(<>) -include::{docdir}/ml/ml-shared.asciidoc[tag=query-delay] - -`script_fields`:: -(object) -include::{docdir}/ml/ml-shared.asciidoc[tag=script-fields] - -`scroll_size`:: -(unsigned integer) -include::{docdir}/ml/ml-shared.asciidoc[tag=scroll-size] +The API returns an array of {dfeed} resources. For the full list of properties, +see <>. [[ml-get-datafeed-response-codes]] ==== {api-response-codes-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc index 6852ca7716d07..d53155681c7fa 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc @@ -37,28 +37,26 @@ You can get a single filter or all filters. For more information, see [[ml-get-filter-query-parms]] ==== {api-query-parms-title} -`from`::: +`from`:: (Optional, integer) Skips the specified number of filters. -`size`::: +`size`:: (Optional, integer) Specifies the maximum number of filters to obtain. [[ml-get-filter-results]] ==== {api-response-body-title} -The API returns the following information: - -`filters`:: - (array) An array of filter resources. A filter resource has the following - properties: - `filter_id`::: - (string) A string that uniquely identifies the filter. +The API returns an array of filter resources, which have the following +properties: - `description`::: - (string) A description of the filter. +`description`:: +(string) A description of the filter. + +`filter_id`:: +(string) A string that uniquely identifies the filter. - `items`::: - (array of strings) An array of strings which is the filter item list. +`items`:: +(array of strings) An array of strings which is the filter item list. [[ml-get-filter-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc index e2727a04dc07c..dd9be3652b3d2 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-influencer.asciidoc @@ -55,10 +55,9 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=exclude-interim-results] (Optional, double) Returns influencers with anomaly scores greater than or equal to this value. -`page`:: -`from`::: +`page`.`from`:: (Optional, integer) Skips the specified number of influencers. -`size`::: +`page`.`size`:: (Optional, integer) Specifies the maximum number of influencers to obtain. `sort`:: diff --git a/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc index 4ef7704fe9fd0..9b9038abaa95b 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc @@ -66,51 +66,51 @@ related error counts. The `data_count` values are cumulative for the lifetime of a job. If a model snapshot is reverted or old results are deleted, the job counts are not reset. -`bucket_count`::: +`data_counts`.`bucket_count`::: (long) The number of bucket results produced by the job. -`earliest_record_timestamp`::: +`data_counts`.`earliest_record_timestamp`::: (date) The timestamp of the earliest chronologically input document. -`empty_bucket_count`::: +`data_counts`.`empty_bucket_count`::: (long) The number of buckets which did not contain any data. If your data contains many empty buckets, consider increasing your `bucket_span` or using functions that are tolerant to gaps in data such as `mean`, `non_null_sum` or `non_zero_count`. -`input_bytes`::: +`data_counts`.`input_bytes`::: (long) The number of bytes of input data posted to the job. -`input_field_count`::: +`data_counts`.`input_field_count`::: (long) The total number of fields in input documents posted to the job. This count includes fields that are not used in the analysis. However, be aware that if you are using a {dfeed}, it extracts only the required fields from the documents it retrieves before posting them to the job. -`input_record_count`::: +`data_counts`.`input_record_count`::: (long) The number of input documents posted to the job. -`invalid_date_count`::: +`data_counts`.`invalid_date_count`::: (long) The number of input documents with either a missing date field or a date that could not be parsed. -`job_id`::: +`data_counts`.`job_id`::: (string) include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] -`last_data_time`::: +`data_counts`.`last_data_time`::: (date) The timestamp at which data was last analyzed, according to server time. -`latest_empty_bucket_timestamp`::: +`data_counts`.`latest_empty_bucket_timestamp`::: (date) The timestamp of the last bucket that did not contain any data. -`latest_record_timestamp`::: +`data_counts`.`latest_record_timestamp`::: (date) The timestamp of the latest chronologically input document. -`latest_sparse_bucket_timestamp`::: +`data_counts`.`latest_sparse_bucket_timestamp`::: (date) The timestamp of the last bucket that was considered sparse. -`missing_field_count`::: +`data_counts`.`missing_field_count`::: (long) The number of input documents that are missing a field that the job is configured to analyze. Input documents with missing fields are still processed because it is possible that not all fields are missing. The value of @@ -123,26 +123,26 @@ necessarily a cause for concern. -- -`out_of_order_timestamp_count`::: +`data_counts`.`out_of_order_timestamp_count`::: (long) The number of input documents that are out of time sequence and outside of the latency window. This information is applicable only when you provide data to the job by using the <>. These out of order documents are discarded, since jobs require time series data to be in ascending chronological order. -`processed_field_count`::: +`data_counts`.`processed_field_count`::: (long) The total number of fields in all the documents that have been processed by the job. Only fields that are specified in the detector configuration object contribute to this count. The timestamp is not included in this count. -`processed_record_count`::: +`data_counts`.`processed_record_count`::: (long) The number of input documents that have been processed by the job. This value includes documents with missing fields, since they are nonetheless analyzed. If you use {dfeeds} and have aggregations in your search query, the `processed_record_count` will be the number of aggregation results processed, not the number of {es} documents. -`sparse_bucket_count`::: +`data_counts`.`sparse_bucket_count`::: (long) The number of buckets that contained few data points compared to the expected number of data points. If your data contains many sparse buckets, consider using a longer `bucket_span`. @@ -157,24 +157,24 @@ NOTE: Unless there is at least one forecast, `memory_bytes`, `records`, -- -`forecasted_jobs`::: +`forecasts_stats`.`forecasted_jobs`::: (long) The number of jobs that have at least one forecast. -`memory_bytes`::: +`forecasts_stats`.`memory_bytes`::: (object) Statistics about the memory usage: minimum, maximum, average and total. -`records`::: +`forecasts_stats`.`records`::: (object) Statistics about the number of forecast records: minimum, maximum, saverage and total. -`processing_time_ms`::: +`forecasts_stats`.`processing_time_ms`::: (object) Statistics about the forecast runtime in milliseconds: minimum, maximum, average and total. -`status`::: +`forecasts_stats`.`status`::: (object) Counts per forecast status. For example: `{"finished" : 2}`. -`total`::: +`forecasts_stats`.`total`::: (long) The number of forecasts currently available for this model. `job_id`:: @@ -185,19 +185,19 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] (object) An object that provides information about the size and contents of the model. It has the following properties: -`bucket_allocation_failures_count`::: +`model_size_stats`.`bucket_allocation_failures_count`::: (long) The number of buckets for which new entities in incoming data were not processed due to insufficient model memory. This situation is also signified by a `hard_limit: memory_status` property value. -`job_id`::: +`model_size_stats`.`job_id`::: (string) include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] -`log_time`::: +`model_size_stats`.`log_time`::: (date) The timestamp of the `model_size_stats` according to server time. -`memory_status`::: +`model_size_stats`.`memory_status`::: (string) The status of the mathematical models. This property can have one of the following values: + @@ -209,22 +209,22 @@ and older unused models will be pruned to free up space. As a result, not all incoming data was processed. -- -`model_bytes`::: +`model_size_stats`.`model_bytes`::: (long) The number of bytes of memory used by the models. This is the maximum value since the last time the model was persisted. If the job is closed, this value indicates the latest size. -`model_bytes_exceeded`::: +`model_size_stats`.`model_bytes_exceeded`::: (long) The number of bytes over the high limit for memory usage at the last allocation failure. -`model_bytes_memory_limit`::: +`model_size_stats`.`model_bytes_memory_limit`::: (long) The upper limit for memory usage, checked on increasing values. -`result_type`::: +`model_size_stats`.`result_type`::: (string) For internal use. The type of result. -`total_by_field_count`::: +`model_size_stats`.`total_by_field_count`::: (long) The number of `by` field values that were analyzed by the models. + -- @@ -233,7 +233,7 @@ partition. -- -`total_over_field_count`::: +`model_size_stats`.`total_over_field_count`::: (long) The number of `over` field values that were analyzed by the models. + -- @@ -242,10 +242,10 @@ partition. -- -`total_partition_field_count`::: +`model_size_stats`.`total_partition_field_count`::: (long) The number of `partition` field values that were analyzed by the models. -`timestamp`::: +`model_size_stats`.`timestamp`::: (date) The timestamp of the `model_size_stats` according to the timestamp of the data. @@ -253,20 +253,20 @@ data. (object) Contains properties for the node that runs the job. This information is available only for open jobs. -`attributes`::: +`node`.`attributes`::: (object) Lists node attributes. For example, `{"ml.machine_memory": "17179869184", "ml.max_open_jobs" : "20"}`. -`ephemeral_id`::: +`node`.`ephemeral_id`::: (string) The ephemeral ID of the node. -`id`::: +`node`.`id`::: (string) The unique identifier of the node. -`name`::: +`node`.`name`::: (string) The node name. -`transport_address`::: +`node`.`transport_address`::: (string) The host and port where transport HTTP connections are accepted. `open_time`:: @@ -295,31 +295,31 @@ then re-opened. (object) An object that provides statistical information about timing aspect of this job. It has the following properties: -`average_bucket_processing_time_ms`::: +`timing_stats`.`average_bucket_processing_time_ms`::: (double) Average of all bucket processing times in milliseconds. -`bucket_count`::: +`timing_stats`.`bucket_count`::: (long) The number of buckets processed. -`exponential_average_bucket_processing_time_ms`::: +`timing_stats`.`exponential_average_bucket_processing_time_ms`::: (double) Exponential moving average of all bucket processing times in milliseconds. -`exponential_average_bucket_processing_time_per_hour_ms`::: +`timing_stats`.`exponential_average_bucket_processing_time_per_hour_ms`::: (double) Exponentially-weighted moving average of bucket processing times calculated in a 1 hour time window. -`job_id`::: +`timing_stats`.`job_id`::: (string) include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] -`maximum_bucket_processing_time_ms`::: +`timing_stats`.`maximum_bucket_processing_time_ms`::: (double) Maximum among all bucket processing times in milliseconds. -`minimum_bucket_processing_time_ms`::: +`timing_stats`.`minimum_bucket_processing_time_ms`::: (double) Minimum among all bucket processing times in milliseconds. -`total_bucket_processing_time_ms`::: +`timing_stats`.`total_bucket_processing_time_ms`::: (double) Sum of all bucket processing times in milliseconds. [[ml-get-job-stats-response-codes]] diff --git a/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc index 7aeafde9ee884..5f4da902734fe 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc @@ -54,87 +54,27 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-jobs] [[ml-get-job-results]] ==== {api-response-body-title} -The API returns an array of {anomaly-job} resources, which have the following -properties: - -`allow_lazy_open`:: -(boolean) -include::{docdir}/ml/ml-shared.asciidoc[tag=allow-lazy-open] - -[[get-analysisconfig]]`analysis_config`:: -(object) -include::{docdir}/ml/ml-shared.asciidoc[tag=analysis-config] - -[[get-analysislimits]]`analysis_limits`:: -(object) -include::{docdir}/ml/ml-shared.asciidoc[tag=analysis-limits] - -`background_persist_interval`:: -(time units) -include::{docdir}/ml/ml-shared.asciidoc[tag=background-persist-interval] +The API returns an array of {anomaly-job} resources. For the full list of +properties, see <>. `create_time`:: (string) The time the job was created. For example, `1491007356077`. This property is informational; you cannot change its value. - -[[get-customsettings]]`custom_settings`:: -(object) -include::{docdir}/ml/ml-shared.asciidoc[tag=custom-settings] - -[[get-datadescription]]`data_description`:: -(object) -include::{docdir}/ml/ml-shared.asciidoc[tag=data-description] - -`description`:: -(string) An optional description of the job. `finished_time`:: (string) If the job closed or failed, this is the time the job finished, otherwise it is `null`. This property is informational; you cannot change its value. -`groups`:: -(array of strings) -include::{docdir}/ml/ml-shared.asciidoc[tag=groups] - -`job_id`:: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-define] -+ --- -This property is informational; you cannot change the identifier for existing -jobs. --- - `job_type`:: (string) Reserved for future use, currently set to `anomaly_detector`. `job_version`:: (string) The version of {es} that existed on the node when the job was created. -[[get-modelplotconfig]]`model_plot_config`:: -(object) -include::{docdir}/ml/ml-shared.asciidoc[tag=model-plot-config] - `model_snapshot_id`:: (string) include::{docdir}/ml/ml-shared.asciidoc[tag=model-snapshot-id] -+ --- -This property is informational; you cannot change its value. --- - -`model_snapshot_retention_days`:: -(long) -include::{docdir}/ml/ml-shared.asciidoc[tag=model-snapshot-retention-days] - -`renormalization_window_days`:: -(long) -include::{docdir}/ml/ml-shared.asciidoc[tag=renormalization-window-days] - -`results_index_name`:: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=results-index-name] [[ml-get-job-response-codes]] ==== {api-response-codes-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc index 7e56fc757cb79..91cc1fa69d046 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-record.asciidoc @@ -59,10 +59,9 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=desc-results] (Optional, boolean) include::{docdir}/ml/ml-shared.asciidoc[tag=exclude-interim-results] -`page`:: -`page`.`from`::: +`page`.`from`:: (Optional, integer) Skips the specified number of records. -`page`.`size`::: +`page`.`size`:: (Optional, integer) Specifies the maximum number of records to obtain. `record_score`:: diff --git a/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc index c241e7acdeb57..8a621f1faafff 100644 --- a/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc @@ -40,18 +40,18 @@ of which must have a start time, end time, and description. and end times may be specified as integer milliseconds since the epoch or as a string in ISO 8601 format. An event resource has the following properties: - `calendar_id`::: +`events`.`calendar_id`::: (Optional, string) An identifier for the calendar that contains the scheduled event. - `description`::: +`events`.`description`::: (Optional, string) A description of the scheduled event. - `end_time`::: +`events`.`end_time`::: (Required, date) The timestamp for the end of the scheduled event in milliseconds since the epoch or ISO 8601 format. - `start_time`::: +`events`.`start_time`::: (Required, date) The timestamp for the beginning of the scheduled event in milliseconds since the epoch or ISO 8601 format. diff --git a/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc index cb3765a86c97e..a5a168b9f9c14 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc @@ -69,10 +69,13 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=frequency] (Required, array) include::{docdir}/ml/ml-shared.asciidoc[tag=indices] - `job_id`:: (Required, string) include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] + +`max_empty_searches`:: +(Optional,integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=max-empty-searches] `query`:: (Optional, object) diff --git a/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc index 15f4ae3466968..16c14c5faae41 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc @@ -46,9 +46,140 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=allow-lazy-open] (Required, object) include::{docdir}/ml/ml-shared.asciidoc[tag=analysis-config] +`analysis_config`.`bucket_span`::: +(<>) +include::{docdir}/ml/ml-shared.asciidoc[tag=bucket-span] + +`analysis_config`.`categorization_field_name`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-field-name] + +`analysis_config`.`categorization_filters`::: +(array of strings) +include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-filters] + +`analysis_config`.`categorization_analyzer`::: +(object or string) +include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-analyzer] + +`analysis_config`.`detectors`::: +(array) An array of detector configuration objects. Detector configuration +objects specify which data fields a job analyzes. They also specify which +analytical functions are used. You can specify multiple detectors for a job. ++ +-- +NOTE: If the `detectors` array does not contain at least one detector, +no analysis can occur and an error is returned. + +A detector has the following properties: +-- + +`analysis_config`.`detectors`.`by_field_name`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=by-field-name] + +`analysis_config`.`detectors`.`custom_rules`:::: ++ +-- +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules] + +`analysis_config`.`detectors`.`custom_rules`.`actions`::: +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-actions] + +`analysis_config`.`detectors`.`custom_rules`.`scope`::: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-scope] + +`analysis_config`.`detectors`.`custom_rules`.`scope`.`filter_id`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-scope-filter-id] + +`analysis_config`.`detectors`.`custom_rules`.`scope`.`filter_type`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-scope-filter-type] + +`analysis_config`.`detectors`.`custom_rules`.`conditions`::: +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions] + +`analysis_config`.`detectors`.`custom_rules`.`conditions`.`applies_to`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions-applies-to] + +`analysis_config`.`detectors`.`custom_rules`.`conditions`.`operator`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions-operator] + +`analysis_config`.`detectors`.`custom_rules`.`conditions`.`value`:::: +(double) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions-value] +-- + +`analysis_config`.`detectors`.`detector_description`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-description] + +`analysis_config`.`detectors`.`detector_index`:::: +(integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-index] + +`analysis_config`.`detectors`.`exclude_frequent`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=exclude-frequent] + +`analysis_config`.`detectors`.`field_name`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-field-name] + +`analysis_config`.`detectors`.`function`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=function] + +`analysis_config`.`detectors`.`over_field_name`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=over-field-name] + +`analysis_config`.`detectors`.`partition_field_name`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=partition-field-name] + +`analysis_config`.`detectors`.`use_null`:::: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=use-null] + +`analysis_config`.`influencers`::: +(array of strings) +include::{docdir}/ml/ml-shared.asciidoc[tag=influencers] + +`analysis_config`.`latency`::: +(time units) +include::{docdir}/ml/ml-shared.asciidoc[tag=latency] + +`analysis_config`.`multivariate_by_fields`::: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=multivariate-by-fields] + +`analysis_config`.`summary_count_field_name`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=summary-count-field-name] + [[put-analysislimits]]`analysis_limits`:: (Optional, object) include::{docdir}/ml/ml-shared.asciidoc[tag=analysis-limits] ++ +-- +The `analysis_limits` object has the following properties: +-- + +`analysis_limits`.`categorization_examples_limit`::: +(long) +include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-examples-limit] + +`analysis_limits`.`model_memory_limit`::: +(long or string) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-memory-limit] `background_persist_interval`:: (Optional, <>) @@ -73,6 +204,14 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=groups] (Optional, object) include::{docdir}/ml/ml-shared.asciidoc[tag=model-plot-config] +`model_plot_config`.`enabled`::: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-plot-config-enabled] + +`model_plot_config`.`terms`::: +experimental[] (string) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-plot-config-terms] + `model_snapshot_retention_days`:: (Optional, long) include::{docdir}/ml/ml-shared.asciidoc[tag=model-snapshot-retention-days] diff --git a/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc index 3a417b6a0fc38..dff75da52f841 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc @@ -43,19 +43,7 @@ close the job, then reopen the job and restart the {dfeed} for the changes to ta -- -`detectors`:: -`custom_rules`::: -(array) -include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules] -`description`::: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=detector-description] -`detector_index`::: -(integer) -include::{docdir}/ml/ml-shared.asciidoc[tag=detector-index] - -[[update-analysislimits]]`analysis_limits`:: -`model_memory_limit`::: +[[update-analysislimits]]`analysis_limits`.`model_memory_limit`:: (long or string) include::{docdir}/ml/ml-shared.asciidoc[tag=model-memory-limit] + @@ -87,6 +75,55 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=custom-settings] `description`:: (string) A description of the job. +`detectors`:: +(array) An array of detector update objects. + +`detectors`.`custom_rules`::: ++ +-- +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules] + +`detectors`.`custom_rules`.`actions`::: +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-actions] + +`detectors`.`custom_rules`.`scope`::: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-scope] + +`detectors`.`custom_rules`.`scope`.`filter_id`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-scope-filter-id] + +`detectors`.`custom_rules`.`scope`.`filter_type`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-scope-filter-type] + +`detectors`.`custom_rules`.`conditions`::: +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions] + +`detectors`.`custom_rules`.`conditions`.`applies_to`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions-applies-to] + +`detectors`.`custom_rules`.`conditions`.`operator`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions-operator] + +`detectors`.`custom_rules`.`conditions`.`value`:::: +(double) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions-value] +-- + +`detectors`.`description`::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-description] +`detectors`.`detector_index`::: +(integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-index] + `groups`:: (array of strings) include::{docdir}/ml/ml-shared.asciidoc[tag=groups] @@ -95,6 +132,14 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=groups] (object) include::{docdir}/ml/ml-shared.asciidoc[tag=model-plot-config] +`model_plot_config`.`enabled`::: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-plot-config-enabled] + +`model_plot_config`.`terms`::: +experimental[] (string) +include::{docdir}/ml/ml-shared.asciidoc[tag=model-plot-config-terms] + `model_snapshot_retention_days`:: (long) include::{docdir}/ml/ml-shared.asciidoc[tag=model-snapshot-retention-days] diff --git a/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc b/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc index cb811f247a1c7..247eb32b10dc7 100644 --- a/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc @@ -29,7 +29,80 @@ before you create an {anomaly-job}. [[ml-valid-detector-request-body]] ==== {api-request-body-title} -include::{docdir}/ml/ml-shared.asciidoc[tag=detector] +`by_field_name`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=by-field-name] + +`custom_rules`:: ++ +-- +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules] + +`analysis_config`.`detectors`.`custom_rules`.`actions`::: +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-actions] + +`analysis_config`.`detectors`.`custom_rules`.`scope`::: +(object) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-scope] + +`analysis_config`.`detectors`.`custom_rules`.`scope`.`filter_id`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-scope-filter-id] + +`analysis_config`.`detectors`.`custom_rules`.`scope`.`filter_type`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-scope-filter-type] + +`analysis_config`.`detectors`.`custom_rules`.`conditions`::: +(array) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions] + +`analysis_config`.`detectors`.`custom_rules`.`conditions`.`applies_to`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions-applies-to] + +`analysis_config`.`detectors`.`custom_rules`.`conditions`.`operator`:::: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions-operator] + +`analysis_config`.`detectors`.`custom_rules`.`conditions`.`value`:::: +(double) +include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules-conditions-value] +-- + +`detector_description`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-description] + +`detector_index`:: +(integer) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-index] + +`exclude_frequent`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=exclude-frequent] + +`field_name`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=detector-field-name] + +`function`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=function] + +`over_field_name`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=over-field-name] + +`partition_field_name`:: +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=partition-field-name] + +`use_null`:: +(boolean) +include::{docdir}/ml/ml-shared.asciidoc[tag=use-null] [[ml-valid-detector-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index 76b1671fed595..6918a3a502796 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -17,7 +17,6 @@ return an error and the job waits in the `opening` state until sufficient {ml} node capacity is available. end::allow-lazy-open[] - tag::allow-lazy-start[] Whether this job should be allowed to start when there is insufficient {ml} node capacity for it to be immediately assigned to a node. The default is `false`, @@ -80,71 +79,16 @@ example: `outlier_detection`. See <>. end::analysis[] tag::analysis-config[] -The analysis configuration, which specifies how to analyze the data. -After you create a job, you cannot change the analysis configuration; all -the properties are informational. An analysis configuration object has the -following properties: - -`bucket_span`::: -(<>) -include::{docdir}/ml/ml-shared.asciidoc[tag=bucket-span] - -`categorization_field_name`::: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-field-name] - -`categorization_filters`::: -(array of strings) -include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-filters] - -`categorization_analyzer`::: -(object or string) -include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-analyzer] - -`detectors`::: -(array) An array of detector configuration objects. Detector configuration -objects specify which data fields a job analyzes. They also specify which -analytical functions are used. You can specify multiple detectors for a job. -include::{docdir}/ml/ml-shared.asciidoc[tag=detector] -+ --- -NOTE: If the `detectors` array does not contain at least one detector, -no analysis can occur and an error is returned. - --- - -`influencers`::: -(array of strings) -include::{docdir}/ml/ml-shared.asciidoc[tag=influencers] - -`latency`::: -(time units) -include::{docdir}/ml/ml-shared.asciidoc[tag=latency] - -`multivariate_by_fields`::: -(boolean) -include::{docdir}/ml/ml-shared.asciidoc[tag=multivariate-by-fields] - -`summary_count_field_name`::: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=summary-count-field-name] - +The analysis configuration, which specifies how to analyze the data. After you +create a job, you cannot change the analysis configuration; all the properties +are informational. end::analysis-config[] tag::analysis-limits[] Limits can be applied for the resources required to hold the mathematical models in memory. These limits are approximate and can be set per job. They do not -control the memory used by other processes, for example the {es} Java -processes. If necessary, you can increase the limits after the job is created. -The `analysis_limits` object has the following properties: - -`categorization_examples_limit`::: -(long) -include::{docdir}/ml/ml-shared.asciidoc[tag=categorization-examples-limit] - -`model_memory_limit`::: -(long or string) -include::{docdir}/ml/ml-shared.asciidoc[tag=model-memory-limit] +control the memory used by other processes, for example the {es} Java processes. +If necessary, you can increase the limits after the job is created. end::analysis-limits[] tag::analyzed-fields[] @@ -212,15 +156,15 @@ object. If it is a string it must refer to a is an object it has the following properties: -- -`char_filter`:::: +`analysis_config`.`categorization_analyzer`.`char_filter`:::: (array of strings or objects) include::{docdir}/ml/ml-shared.asciidoc[tag=char-filter] -`tokenizer`:::: +`analysis_config`.`categorization_analyzer`.`tokenizer`:::: (string or object) include::{docdir}/ml/ml-shared.asciidoc[tag=tokenizer] -`filter`:::: +`analysis_config`.`categorization_analyzer`.`filter`:::: (array of strings or objects) include::{docdir}/ml/ml-shared.asciidoc[tag=filter] end::categorization-analyzer[] @@ -286,11 +230,11 @@ on {es} is managed. Chunking configuration controls how the size of these time chunks are calculated and is an advanced configuration option. A chunking configuration object has the following properties: -`mode`::: +`chunking_config`.`mode`::: (string) include::{docdir}/ml/ml-shared.asciidoc[tag=mode] -`time_span`::: +`chunking_config`.`time_span`::: (<>) include::{docdir}/ml/ml-shared.asciidoc[tag=time-span] end::chunking-config[] @@ -300,11 +244,10 @@ An array of custom rule objects, which enable you to customize the way detectors operate. For example, a rule may dictate to the detector conditions under which results should be skipped. For more examples, see {ml-docs}/ml-configuring-detector-custom-rules.html[Customizing detectors with custom rules]. -A custom rule has the following properties: -+ --- -`actions`:: -(array) The set of actions to be triggered when the rule applies. If +end::custom-rules[] + +tag::custom-rules-actions[] +The set of actions to be triggered when the rule applies. If more than one action is specified the effects of all actions are combined. The available actions include: @@ -316,49 +259,47 @@ model. Unless you also specify `skip_result`, the results will be created as usual. This action is suitable when certain values are expected to be consistently anomalous and they affect the model in a way that negatively impacts the rest of the results. +end::custom-rules-actions[] -`scope`:: -(object) An optional scope of series where the rule applies. A rule must either +tag::custom-rules-scope[] +An optional scope of series where the rule applies. A rule must either have a non-empty scope or at least one condition. By default, the scope includes all series. Scoping is allowed for any of the fields that are also specified in `by_field_name`, `over_field_name`, or `partition_field_name`. To add a scope for a field, add the field name as a key in the scope object and set its value to an object with the following properties: - -`filter_id`::: -(string) The id of the filter to be used. - -`filter_type`::: -(string) Either `include` (the rule applies for values in the filter) or -`exclude` (the rule applies for values not in the filter). Defaults to -`include`. - -`conditions`:: -(array) An optional array of numeric conditions when the rule applies. A rule -must either have a non-empty scope or at least one condition. Multiple -conditions are combined together with a logical `AND`. A condition has the -following properties: - -`applies_to`::: -(string) Specifies the result property to which the condition applies. The -available options are `actual`, `typical`, `diff_from_typical`, `time`. - -`operator`::: -(string) Specifies the condition operator. The available options are `gt` -(greater than), `gte` (greater than or equals), `lt` (less than) and `lte` (less -than or equals). - -`value`::: -(double) The value that is compared against the `applies_to` field using the -`operator`. --- -+ --- -NOTE: If your detector uses `lat_long`, `metric`, `rare`, or `freq_rare` -functions, you can only specify `conditions` that apply to `time`. - --- -end::custom-rules[] +end::custom-rules-scope[] + +tag::custom-rules-scope-filter-id[] +The id of the filter to be used. +end::custom-rules-scope-filter-id[] + +tag::custom-rules-scope-filter-type[] +Either `include` (the rule applies for values in the filter) or `exclude` (the +rule applies for values not in the filter). Defaults to `include`. +end::custom-rules-scope-filter-type[] + +tag::custom-rules-conditions[] +An optional array of numeric conditions when the rule applies. A rule must +either have a non-empty scope or at least one condition. Multiple conditions are +combined together with a logical `AND`. A condition has the following properties: +end::custom-rules-conditions[] + +tag::custom-rules-conditions-applies-to[] +Specifies the result property to which the condition applies. The available +options are `actual`, `typical`, `diff_from_typical`, `time`. If your detector +uses `lat_long`, `metric`, `rare`, or `freq_rare` functions, you can only +specify conditions that apply to `time`. +end::custom-rules-conditions-applies-to[] + +tag::custom-rules-conditions-operator[] +Specifies the condition operator. The available options are `gt` (greater than), +`gte` (greater than or equals), `lt` (less than) and `lte` (less than or equals). +end::custom-rules-conditions-operator[] + +tag::custom-rules-conditions-value[] +The value that is compared against the `applies_to` field using the `operator`. +end::custom-rules-conditions-value[] tag::custom-settings[] Advanced configuration option. Contains custom meta data about the job. For @@ -375,16 +316,14 @@ a {dfeed}, these properties are automatically set. When data is received via the <> API, it is not stored in {es}. Only the results for {anomaly-detect} are retained. -A data description object has the following properties: - -`format`::: +`data_description`.`format`::: (string) Only `JSON` format is supported at this time. -`time_field`::: +`data_description`.`time_field`::: (string) The name of the field that contains the timestamp. The default value is `time`. -`time_format`::: +`data_description`.`time_format`::: (string) include::{docdir}/ml/ml-shared.asciidoc[tag=time-format] -- @@ -507,13 +446,11 @@ moment in time. See This check runs only on real-time {dfeeds}. -The configuration object has the following properties: - -`enabled`:: +`delayed_data_check_config`.`enabled`:: (boolean) Specifies whether the {dfeed} periodically checks for delayed data. Defaults to `true`. -`check_window`:: +`delayed_data_check_config`.`check_window`:: (<>) The window of time that is searched for late data. This window of time ends with the latest finalized bucket. It defaults to `null`, which causes an appropriate `check_window` to be calculated when the @@ -571,51 +508,6 @@ the detectors in the `analysis_config`, starting at zero. You can use this identifier when you want to update a specific detector. end::detector-index[] -tag::detector[] -A detector has the following properties: - -`by_field_name`:::: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=by-field-name] - -`custom_rules`:::: -(array) -include::{docdir}/ml/ml-shared.asciidoc[tag=custom-rules] - -`detector_description`:::: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=detector-description] - -`detector_index`:::: -(integer) -include::{docdir}/ml/ml-shared.asciidoc[tag=detector-index] - -`exclude_frequent`:::: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=exclude-frequent] - -`field_name`:::: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=detector-field-name] - -`function`:::: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=function] - -`over_field_name`:::: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=over-field-name] - -`partition_field_name`:::: -(string) -include::{docdir}/ml/ml-shared.asciidoc[tag=partition-field-name] - -`use_null`:::: -(boolean) -include::{docdir}/ml/ml-shared.asciidoc[tag=use-null] - -end::detector[] - tag::eta[] The shrinkage applied to the weights. Smaller values result in larger forests which have better generalization error. However, the smaller @@ -911,22 +803,21 @@ be seen in the model plot. Model plot config can be configured when the job is created or updated later. It must be disabled if performance issues are experienced. - -The `model_plot_config` object has the following properties: - -`enabled`::: -(boolean) If true, enables calculation and storage of the model bounds for -each entity that is being analyzed. By default, this is not enabled. - -`terms`::: -experimental[] (string) Limits data collection to this comma separated list of -partition or by field values. If terms are not specified or it is an empty -string, no filtering is applied. For example, "CPU,NetworkIn,DiskWrites". -Wildcards are not supported. Only the specified `terms` can be viewed when -using the Single Metric Viewer. -- end::model-plot-config[] +tag::model-plot-config-enabled[] +If true, enables calculation and storage of the model bounds for each entity +that is being analyzed. By default, this is not enabled. +end::model-plot-config-enabled[] + +tag::model-plot-config-terms[] +Limits data collection to this comma separated list of partition or by field +values. If terms are not specified or it is an empty string, no filtering is +applied. For example, "CPU,NetworkIn,DiskWrites". Wildcards are not supported. +Only the specified `terms` can be viewed when using the Single Metric Viewer. +end::model-plot-config-terms[] + tag::model-snapshot-id[] A numerical character string that uniquely identifies the model snapshot. For example, `1575402236000 `. From 0b445e1db55124fb02bc7de5c9d10ab691087206 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Thu, 26 Dec 2019 07:41:23 -0500 Subject: [PATCH 334/686] [DOCS] Document `transport` and `http` node stats (#50473) Documents the `transport` and `http` parameters returned by the `_nodes/stats` API. --- docs/reference/cluster/nodes-stats.asciidoc | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/reference/cluster/nodes-stats.asciidoc b/docs/reference/cluster/nodes-stats.asciidoc index af3ad0deee442..9306c23e43f43 100644 --- a/docs/reference/cluster/nodes-stats.asciidoc +++ b/docs/reference/cluster/nodes-stats.asciidoc @@ -922,6 +922,44 @@ Highest number of active threads in the thread pool. (integer) Number of tasks completed by the thread pool executor. +[[cluster-nodes-stats-api-response-body-transport]] +===== `transport` section + +`transport.server_open`:: +(integer) +Number of open TCP connections used for internal communication between nodes. + +`transport.rx_count`:: +(integer) +Total number of RX (receive) packets received by the node during internal +cluster communication. + +`transport.rx_size_in_bytes`:: +(integer) +Size, in bytes, of RX packets received by the node during internal cluster +communication. + +`transport.tx_count`:: +(integer) +Total number of TX (transmit) packets sent by the node during internal cluster +communication. + +`transport.tx_size_in_bytes`:: +(integer) +Size, in bytes, of TX packets sent by the node during internal cluster +communication. + +[[cluster-nodes-stats-api-response-body-http]] +===== `http` section + +`http.current_open`:: +(integer) +Current number of open HTTP connections for the node. + +`http.total_opened`:: +(integer) +Total number of HTTP connections opened for the node. + [[cluster-nodes-stats-api-response-body-ingest]] ===== `ingest` section From 85a38087a11e8aa48178268cf39735f09e7df4d7 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Thu, 26 Dec 2019 07:49:41 -0500 Subject: [PATCH 335/686] [DOCS] Remove unneeded redirects (#50476) The docs/reference/redirects.asciidoc file stores a list of relocated or deleted pages for the Elasticsearch Reference documentation. This prunes several older redirects that are no longer needed and don't require work to fix broken links in other repositories. --- docs/reference/how-to/search-speed.asciidoc | 2 +- .../mapping/removal_of_types.asciidoc | 2 +- .../ml/anomaly-detection/apis/ml-api.asciidoc | 4 +- docs/reference/redirects.asciidoc | 596 ------------------ .../security/create-api-keys.asciidoc | 2 +- .../security/create-role-mappings.asciidoc | 4 +- .../configuring-kerberos-realm.asciidoc | 8 +- .../en/security/fips-140-compliance.asciidoc | 3 +- 8 files changed, 12 insertions(+), 609 deletions(-) diff --git a/docs/reference/how-to/search-speed.asciidoc b/docs/reference/how-to/search-speed.asciidoc index a275af5fead29..5d0e004a7fb6a 100644 --- a/docs/reference/how-to/search-speed.asciidoc +++ b/docs/reference/how-to/search-speed.asciidoc @@ -31,7 +31,7 @@ If your search is CPU-bound, you should investigate buying faster CPUs. Documents should be modeled so that search-time operations are as cheap as possible. In particular, joins should be avoided. <> can make queries -several times slower and <> relations can make +several times slower and <> relations can make queries hundreds of times slower. So if the same questions can be answered without joins by denormalizing documents, significant speedups can be expected. diff --git a/docs/reference/mapping/removal_of_types.asciidoc b/docs/reference/mapping/removal_of_types.asciidoc index d2f5eef943df7..1c299bfc073c2 100644 --- a/docs/reference/mapping/removal_of_types.asciidoc +++ b/docs/reference/mapping/removal_of_types.asciidoc @@ -42,7 +42,7 @@ field, so documents of different types with the same `_id` could exist in a single index. Mapping types were also used to establish a -<> +<> between documents, so documents of type `question` could be parents to documents of type `answer`. diff --git a/docs/reference/ml/anomaly-detection/apis/ml-api.asciidoc b/docs/reference/ml/anomaly-detection/apis/ml-api.asciidoc index 2014d05812595..f02312cb0ac94 100644 --- a/docs/reference/ml/anomaly-detection/apis/ml-api.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/ml-api.asciidoc @@ -3,9 +3,7 @@ [[ml-apis]] == {ml-cap} {anomaly-detect} APIs -You can use the following APIs to perform {ml} {anomaly-detect} activities. See -<> for the resource definitions used by the -machine learning APIs and in advanced job configuration options in Kibana. +You can use the following APIs to perform {ml} {anomaly-detect} activities. See also <>. diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 8580705623146..4c5e26bb2e799 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -3,619 +3,23 @@ The following pages have moved or been deleted. -[role="exclude",id="cluster-nodes-shutdown"] -=== Nodes shutdown - -The `_shutdown` API has been removed. Instead, setup Elasticsearch to run as -a service (see <>, <>, or <>) or use the `-p` -command line option to <>. - [role="exclude",id="indices-upgrade"] === Upgrade API The `_upgrade` API is no longer useful and will be removed. Instead, see <>. -[role="exclude",id="migration-api-assistance"] -=== Migration Assistance API - -The Migration Assistance API has been replaced with the -<>. - -[role="exclude",id="migration-api-upgrade"] -=== Migration Upgrade API - -The Migration Upgrade API has been removed. Use the -{kibana-ref}/upgrade-assistant.html[{kib} Upgrade Assistant] or -<> instead. - -[role="exclude",id="docs-bulk-udp"] -=== Bulk UDP API - -The Bulk UDP services has been removed. Use the standard <> instead. - -[role="exclude",id="indices-delete-mapping"] -=== Delete Mapping - -It is no longer possible to delete the mapping for a type. Instead you should -<> and recreate it with the new mappings. - -[role="exclude",id="indices-status"] -=== Index Status - -The index `_status` API has been replaced with the <> and -<> APIs. - -[role="exclude",id="mapping-analyzer-field"] -=== `_analyzer` - -The `_analyzer` field in type mappings is no longer supported and will be -automatically removed from mappings when upgrading to 2.x. - -[role="exclude",id="mapping-boost-field"] -=== `_boost` - -The `_boost` field in type mappings is no longer supported and will be -automatically removed from mappings when upgrading to 2.x. - -[role="exclude",id="mapping-conf-mappings"] -=== Config mappings - -It is no longer possible to specify mappings in files in the `config` -directory. Instead, mappings should be created using the API with: - -* <> -* <> -* <> - [role="exclude",id="mapping-parent-field"] === `_parent` field The `_parent` field has been removed in favour of the <>. -[role="exclude",id="mapping-uid-field"] -=== `_uid` field - -The `_uid` field has been removed in favour of the <>. - -[role="exclude",id="modules-memcached"] -=== memcached - -The `memcached` transport is no longer supported. Instead use the REST -interface over <>. - -[role="exclude",id="modules-thrift"] -=== Thrift - -The `thrift` transport is no longer supported. Instead use the REST -interface over <>. - -// QUERY DSL - -[role="exclude",id="query-dsl-queries"] -=== Queries - -Queries and filters have been merged. Any query clause can now be used as a query -in ``query context'' and as a filter in ``filter context'' (see <>). - -[role="exclude",id="query-dsl-filters"] -=== Filters - -Queries and filters have been merged. Any query clause can now be used as a query -in ``query context'' and as a filter in ``filter context'' (see <>). - -[role="exclude",id="query-dsl-not-filter"] -=== Not Filter - -The `not` query has been replaced by using a `must_not` clause in a `bool` query (see <>). - -[role="exclude",id="query-dsl-bool-filter"] -=== Bool Filter - -The `bool` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-exists-filter"] -=== Exists Filter - -The `exists` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-geo-bounding-box-filter"] -=== Geo Bounding Box Filter - -The `geo_bounding_box` filter has been replaced by the <>. -It behaves as a query in ``query context'' and as a filter in ``filter -context'' (see <>). - -[role="exclude",id="query-dsl-geo-distance-filter"] -=== Geo Distance Filter - -The `geo_distance` filter has been replaced by the <>. -It behaves as a query in ``query context'' and as a filter in ``filter -context'' (see <>). - -[role="exclude",id="query-dsl-geo-distance-range-filter"] -=== Geo Distance Range Filter - -The `geo_distance_range` filter has been replaced by the <>. -It behaves as a query in ``query context'' and as a filter in ``filter -context'' (see <>). - -[role="exclude",id="query-dsl-geo-polygon-filter"] -=== Geo Polygon Filter - -The `geo_polygon` filter has been replaced by the <>. -It behaves as a query in ``query context'' and as a filter in ``filter -context'' (see <>). - -[role="exclude",id="query-dsl-geo-shape-filter"] -=== Geo Shape Filter - -The `geo_shape` filter has been replaced by the <>. -It behaves as a query in ``query context'' and as a filter in ``filter -context'' (see <>). - -[role="exclude",id="query-dsl-has-child-filter"] -=== Has Child Filter - -The `has_child` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-has-parent-filter"] -=== Has Parent Filter - -The `has_parent` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-top-children-query"] -=== Top Children Query - -The `top_children` query has been removed. Use the <> instead. - -[role="exclude",id="query-dsl-ids-filter"] -=== IDs Filter - -The `ids` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-match-all-filter"] -=== Match All Filter - -The `match_all` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-nested-filter"] -=== Nested Filter - -The `nested` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-prefix-filter"] -=== Prefix Filter - -The `prefix` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-query-filter"] -=== Query Filter - -The `query` filter has been removed as queries and filters have been merged (see -<>). - -[role="exclude",id="query-dsl-range-filter"] -=== Range Filter - -The `range` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-regexp-filter"] -=== Regexp Filter - -The `regexp` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-script-filter"] -=== Script Filter - -The `script` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-term-filter"] -=== Term Filter - -The `term` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-terms-filter"] -=== Terms Filter - -The `terms` filter has been replaced by the <>. It behaves -as a query in ``query context'' and as a filter in ``filter context'' (see -<>). - -[role="exclude",id="query-dsl-flt-query"] -=== Fuzzy Like This Query - -The `fuzzy_like_this`, alternatively known as `flt`, query has been removed. Instead use either -the <> parameter with the -<> or the <>. - - -[role="exclude",id="query-dsl-flt-field-query"] -=== Fuzzy Like This Field Query - -The `fuzzy_like_this_field` or `flt_field` query has been removed. Instead use -the <> parameter with the -<> or the <>. - -[role="exclude",id="query-dsl-geo-distance-range-query"] -=== Geo distance range Query - -The `geo_distance_range` query has been removed. Instead use the -<> with pagination -or the -<> -depending on your needs. - -[role="exclude",id="query-dsl-geohash-cell-query"] -=== Geohash Cell Query - -The `geohash_cell` query has been removed. Instead use the -<>. - -[role="exclude",id="search-more-like-this"] -=== More Like This API - -The More Like This API has been removed. Instead, use the <>. - -// FACETS - -[role="exclude",id="search-facets"] -=== Facets - -Faceted search refers to a way to explore large amounts of data by displaying -summaries about various partitions of the data and later allowing to narrow -the navigation to a specific partition. - -In Elasticsearch, `facets` are also the name of a feature that allowed to -compute these summaries. `facets` have been replaced by -<> in Elasticsearch 1.0, which are a superset -of facets. - -[role="exclude",id="search-facets-filter-facet"] -=== Filter Facet - -Facets have been removed. Use the -<> or -<> instead. - -[role="exclude",id="search-facets-query-facet"] -=== Query Facet - -Facets have been removed. Use the -<> or -<> instead. - -[role="exclude",id="search-facets-geo-distance-facet"] -=== Geo Distance Facet - -Facets have been removed. Use the -<> instead. - -[role="exclude",id="search-facets-histogram-facet"] -=== Histogram Facet - -Facets have been removed. Use the -<> instead. - -[role="exclude",id="search-facets-date-histogram-facet"] -=== Date Histogram Facet - -Facets have been removed. Use the -<> instead. - -[role="exclude",id="search-facets-range-facet"] -=== Range Facet - -Facets have been removed. Use the -<> instead. - -[role="exclude",id="search-facets-terms-facet"] -=== Terms Facet - -Facets have been removed. Use the -<> instead. - -[role="exclude",id="search-facets-terms-statistical-facet"] -=== Terms Stats Facet - -Facets have been removed. Use the -<> -with the <> -or the <> -instead. - -[role="exclude",id="search-facets-statistical-facet"] -=== Statistical Facet - -Facets have been removed. Use the -<> -or the <> instead. - -[role="exclude",id="search-facets-migrating-to-aggs"] -=== Migrating from facets to aggregations - -Facets have been removed. Use <> instead. - -// CACHES - -[role="exclude",id="shard-query-cache"] -=== Shard request cache - -The shard query cache has been renamed <>. - -[role="exclude",id="filter-cache"] -=== Query cache - -The filter cache has been renamed <>. - -[role="exclude",id="query-dsl-filtered-query"] -=== Filtered query - -The `filtered` query is replaced by the <> query. Instead of -the following: - -[source,js] -------------------------- -## INCORRECT - DEPRECATED SYNTAX, DO NOT USE -GET _search -{ - "query": { - "filtered": { - "query": { - "match": { - "text": "quick brown fox" - } - }, - "filter": { - "term": { - "status": "published" - } - } - } - } -} -------------------------- -// NOTCONSOLE - -move the query and filter to the `must` and `filter` parameters in the `bool` -query: - -[source,console] -------------------------- -GET _search -{ - "query": { - "bool": { - "must": { - "match": { - "text": "quick brown fox" - } - }, - "filter": { - "term": { - "status": "published" - } - } - } - } -} -------------------------- - -[role="exclude",id="query-dsl-or-query"] -=== Or query - -The `or` query is replaced in favour of the <> query. - -[role="exclude",id="query-dsl-or-filter"] -=== Or filter - -The `or` filter is replaced in favour of the <> query. - -[role="exclude",id="query-dsl-and-query"] -=== And query - -The `and` query is replaced in favour of the <> query. - -[role="exclude",id="query-dsl-and-filter"] -=== And filter - -The `and` filter is replaced in favour of the <> query. - -[role="exclude",id="query-dsl-limit-query"] -=== Limit query - -The `limit` query is replaced in favour of the <> -parameter of search requests. - -[role="exclude",id="query-dsl-limit-filter"] -=== Limit filter - -The `limit` filter is replaced in favour of the <> -parameter of search requests. - -[role="exclude",id="query-dsl-not-query"] -=== Not query - -The `not` query has been replaced by using a `mustNot` clause in a Boolean query. - -[role="exclude",id="mapping-nested-type"] -=== Nested type - -The docs for the `nested` field datatype have moved to <>. - [role="exclude",id="indices-warmers"] === Warmers Warmers have been removed. There have been significant improvements to the index that make warmers not necessary anymore. -[role="exclude",id="index-boost"] -=== Index time boosting - -The index time boost mapping has been replaced with query time boost (see <>). - -[role="exclude",id="modules-scripting-native"] -=== Native scripting - -Native scripts have been replaced with writing custom `ScriptEngine` backends (see <>). - -[role="exclude",id="modules-advanced-scripting"] -=== Advanced scripting - -Using `_index` in scripts has been replaced with writing `ScriptEngine` backends (see <>). - -[role="exclude",id="modules-scripting-painless-syntax"] -=== Painless Syntax - -See the -{painless}/painless-lang-spec.html[Painless Language Specification] -in the guide to the {painless}/index.html[Painless Scripting Language]. - -[role="exclude",id="modules-scripting-painless-debugging"] -=== Painless Debugging - -See {painless}/painless-debugging.html[Painless Debugging] in the -guide to the {painless}/index.html[Painless Scripting Language]. - -[role="exclude",id="painless-api-reference"] -=== Painless Contexts API Reference - -See the {painless}/painless-api-reference.html[Painless Contexts API Reference] -in the guide to the {painless}/index.html[Painless Scripting Language]. - -[role="exclude", id="security-api-roles"] -=== Role management APIs - -You can use the following APIs to add, remove, and retrieve roles in the native realm: - -* <>, <> -* <> -* <> - -[role="exclude",id="security-api-tokens"] -=== Token management APIs - -You can use the following APIs to create and invalidate bearer tokens for access -without requiring basic authentication: - -* <>, <> - -[role="exclude",id="security-api-users"] -=== User Management APIs - -You can use the following APIs to create, read, update, and delete users from the -native realm: - -* <>, <> -* <>, <> -* <> -* <> - -[role="exclude",id="security-api-role-mapping"] -=== Role mapping APIs - -You can use the following APIs to add, remove, and retrieve role mappings: - -* <>, <> -* <> - -[role="exclude",id="security-api-privileges"] -=== Privilege APIs - -See <>. - -[role="exclude",id="xpack-commands"] -=== X-Pack commands - -See <>. - -[role="exclude",id="ml-api-definitions"] -=== Machine learning API definitions - -See <>. - -[role="exclude",id="analysis-standard-tokenfilter"] -=== Standard filter removed - -The standard token filter has been removed. - -[role="exclude",id="modules-discovery-azure-classic"] - -See <>. - -[role="exclude",id="modules-discovery-ec2"] - -See <>. - -[role="exclude",id="modules-discovery-gce"] - -See <>. - -[role="exclude",id="modules-discovery-zen"] - -Zen discovery is replaced by the <>. - -[role="exclude",id="settings-xpack"] -=== {xpack} settings in {es} - -include::{asciidoc-dir}/../../shared/settings.asciidoc[] - -[role="exclude",id="_faster_phrase_queries_with_literal_index_phrases_literal"] - -See <>. - -[role="exclude",id="_faster_prefix_queries_with_literal_index_prefixes_literal.html"] - -See <>. - -[role="exclude",id="setup-xpack"] -=== Set up {xpack} - -{xpack} is an Elastic Stack extension that provides security, alerting, -monitoring, reporting, machine learning, and many other capabilities. By default, -when you install {es}, {xpack} is installed. - -[role="exclude",id="setup-xpack-client"] -=== Configuring {xpack} Java Clients - -The `TransportClient` is deprecated in favour of the -{java-rest}/java-rest-high.html[Java High Level REST Client] and was removed in -Elasticsearch 8.0. The -{java-rest}/java-rest-high-level-migration.html[migration guide] describes all -the steps needed to migrate. - -[role="exclude",id="query-dsl-common-terms-query"] -=== Common Terms Query - -The `common` terms query is deprecated. Use the <> instead. The `match` query skips blocks of documents efficiently, -without any configuration, if the total number of hits is not tracked. - [role="exclude",id="indices-types-exists"] === Types Exists diff --git a/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc index 801064dca452e..015fb4dc03130 100644 --- a/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc @@ -54,7 +54,7 @@ authenticated user_. If you supply role descriptors then the resultant permissio would be an intersection of API keys permissions and authenticated user's permissions thereby limiting the access scope for API keys. The structure of role descriptor is the same as the request for create role API. -For more details, see <>. +For more details, see <>. `expiration`:: (Optional, string) Expiration time for the API key. By default, API keys never diff --git a/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc b/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc index b03bee933fdc8..e80ff662c8305 100644 --- a/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc @@ -32,8 +32,8 @@ The create or update role mappings API cannot update role mappings that are defi in role mapping files. NOTE: This API does not create roles. Rather, it maps users to existing roles. -Roles can be created by using <> or -<>. +Roles can be created by using the <> or <>. For more information, see <>. diff --git a/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc index 9f20596837a0e..6448e96ca1da8 100644 --- a/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc @@ -139,10 +139,10 @@ see <>. + -- -The `kerberos` realm enables you to map Kerberos users to roles. You can -configure these role mappings by using the -<>. You identify -users by their `username` field. +The `kerberos` realm enables you to map Kerberos users to roles. You can +configure these role mappings by using the +<>. You +identify users by their `username` field. The following example uses the role mapping API to map `user@REALM` to the roles `monitoring` and `user`: diff --git a/x-pack/docs/en/security/fips-140-compliance.asciidoc b/x-pack/docs/en/security/fips-140-compliance.asciidoc index 8f5956068cb8b..a700ead8a8ee0 100644 --- a/x-pack/docs/en/security/fips-140-compliance.asciidoc +++ b/x-pack/docs/en/security/fips-140-compliance.asciidoc @@ -100,7 +100,8 @@ on disk. Authentication will still work, but in order to ensure FIPS 140-2 compliance, you would need to recreate users or change their password using the <> CLI tool for the file realm and the -<> for the native realm. +<> and <> APIs for the native realm. The user cache will be emptied upon node restart, so any existing hashes using non-compliant algorithms will be discarded and the new ones will be created From 6b5ee4d419ee06e921ca4815cd3ec8217216c637 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 26 Dec 2019 08:56:00 -0500 Subject: [PATCH 336/686] Fix testCancelRecoveryDuringPhase1 (#50449) testCancelRecoveryDuringPhase1 uses a mock of IndexShard, which can't create retention leases. We need to stub method createRetentionLease. Relates #50351 Closes #50424 --- .../indices/recovery/RecoverySourceHandler.java | 2 +- .../indices/recovery/RecoverySourceHandlerTests.java | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java index 07db659299e7c..3e132c979d7b6 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java @@ -555,7 +555,7 @@ void phase1(IndexCommit snapshot, long startingSeqNo, IntSupplier translogOps, A } } - private void createRetentionLease(final long startingSeqNo, ActionListener listener) { + void createRetentionLease(final long startingSeqNo, ActionListener listener) { runUnderPrimaryPermit(() -> { // Clone the peer recovery retention lease belonging to the source shard. We are retaining history between the the local // checkpoint of the safe commit we're creating and this lease's retained seqno with the retention lock, and by cloning an diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java index a10a8fc0410b6..999028cb083fd 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java @@ -64,6 +64,7 @@ import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.seqno.ReplicationTracker; +import org.elasticsearch.index.seqno.RetentionLease; import org.elasticsearch.index.seqno.RetentionLeases; import org.elasticsearch.index.seqno.SeqNoStats; import org.elasticsearch.index.seqno.SequenceNumbers; @@ -628,7 +629,6 @@ public void writeFileChunk(StoreFileMetaData md, long position, BytesReference c store.close(); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50424") public void testCancelRecoveryDuringPhase1() throws Exception { Store store = newStore(createTempDir("source"), false); IndexShard shard = mock(IndexShard.class); @@ -677,8 +677,16 @@ public void cleanFiles(int totalTranslogOps, long globalCheckpoint, Store.Metada } } }; + final StartRecoveryRequest startRecoveryRequest = getStartRecoveryRequest(); final RecoverySourceHandler handler = new RecoverySourceHandler( - shard, recoveryTarget, threadPool, getStartRecoveryRequest(), between(1, 16), between(1, 4)); + shard, recoveryTarget, threadPool, startRecoveryRequest, between(1, 16), between(1, 4)) { + @Override + void createRetentionLease(long startingSeqNo, ActionListener listener) { + final String leaseId = ReplicationTracker.getPeerRecoveryRetentionLeaseId(startRecoveryRequest.targetNode().getId()); + listener.onResponse(new RetentionLease(leaseId, startingSeqNo, threadPool.absoluteTimeInMillis(), + ReplicationTracker.PEER_RECOVERY_RETENTION_LEASE_SOURCE)); + } + }; cancelRecovery.set(() -> handler.cancel("test")); final StepListener phase1Listener = new StepListener<>(); try { From 3a792ba10611cbc2c23b291c0f8eaeb9a2ca76e1 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 26 Dec 2019 08:58:34 -0500 Subject: [PATCH 337/686] Ensure relocating shards establish peer recovery retention leases (#50486) We forgot to establish peer recovery retention leases for relocating primaries without soft-deletes. Relates #50351 --- .../index/seqno/ReplicationTracker.java | 6 +-- .../elasticsearch/recovery/RelocationIT.java | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java index aff85f10e4bf8..8071d0e4b7d79 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java @@ -1334,11 +1334,7 @@ public synchronized void activateWithPrimaryContext(PrimaryContext primaryContex // note that if there was no cluster state update between start of the engine of this shard and the call to // initializeWithPrimaryContext, we might still have missed a cluster state update. This is best effort. runAfter.run(); - - if (indexSettings.isSoftDeleteEnabled()) { - addPeerRecoveryRetentionLeaseForSolePrimary(); - } - + addPeerRecoveryRetentionLeaseForSolePrimary(); assert invariant(); } diff --git a/server/src/test/java/org/elasticsearch/recovery/RelocationIT.java b/server/src/test/java/org/elasticsearch/recovery/RelocationIT.java index 2d0856c95ec40..c22d1bc0dd28f 100644 --- a/server/src/test/java/org/elasticsearch/recovery/RelocationIT.java +++ b/server/src/test/java/org/elasticsearch/recovery/RelocationIT.java @@ -20,12 +20,14 @@ package org.elasticsearch.recovery; import com.carrotsearch.hppc.IntHashSet; +import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.procedures.IntProcedure; import org.apache.lucene.index.IndexFileNames; import org.apache.lucene.util.English; import org.elasticsearch.action.ActionFuture; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; import org.elasticsearch.action.admin.cluster.reroute.ClusterRerouteResponse; +import org.elasticsearch.action.admin.indices.stats.ShardStats; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchResponse; @@ -45,6 +47,9 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.seqno.ReplicationTracker; +import org.elasticsearch.index.seqno.RetentionLease; import org.elasticsearch.index.shard.IndexEventListener; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShardState; @@ -77,9 +82,12 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; @@ -88,6 +96,8 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchHits; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.in; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; @@ -103,6 +113,7 @@ protected Collection> nodePlugins() { @Override protected void beforeIndexDeletion() throws Exception { super.beforeIndexDeletion(); + assertActiveCopiesEstablishedPeerRecoveryRetentionLeases(); internalCluster().assertSeqNos(); internalCluster().assertSameDocIdsOnShards(); } @@ -603,6 +614,49 @@ public void testRelocateWhileContinuouslyIndexingAndWaitingForRefresh() throws E assertThat(client().prepareSearch("test").setSize(0).execute().actionGet().getHits().getTotalHits().value, equalTo(120L)); } + public void testRelocationEstablishedPeerRecoveryRetentionLeases() throws Exception { + int halfNodes = randomIntBetween(1, 3); + String indexName = "test"; + Settings[] nodeSettings = Stream.concat( + Stream.generate(() -> Settings.builder().put("node.attr.color", "blue").build()).limit(halfNodes), + Stream.generate(() -> Settings.builder().put("node.attr.color", "red").build()).limit(halfNodes)).toArray(Settings[]::new); + List nodes = internalCluster().startNodes(nodeSettings); + String[] blueNodes = nodes.subList(0, halfNodes).toArray(String[]::new); + String[] redNodes = nodes.subList(0, halfNodes).toArray(String[]::new); + ensureStableCluster(halfNodes * 2); + assertAcked( + client().admin().indices().prepareCreate(indexName).setSettings(Settings.builder() + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, halfNodes - 1)) + .put("index.routing.allocation.include.color", "blue"))); + ensureGreen("test"); + assertBusy(() -> assertAllShardsOnNodes(indexName, blueNodes)); + assertActiveCopiesEstablishedPeerRecoveryRetentionLeases(); + client().admin().indices().prepareUpdateSettings(indexName) + .setSettings(Settings.builder().put("index.routing.allocation.include.color", "red")).get(); + assertBusy(() -> assertAllShardsOnNodes(indexName, redNodes)); + ensureGreen("test"); + assertActiveCopiesEstablishedPeerRecoveryRetentionLeases(); + } + + private void assertActiveCopiesEstablishedPeerRecoveryRetentionLeases() throws Exception { + assertBusy(() -> { + for (ObjectCursor it : client().admin().cluster().prepareState().get().getState().metaData().indices().keys()) { + Map> byShardId = Stream.of(client().admin().indices().prepareStats(it.value).get().getShards()) + .collect(Collectors.groupingBy(l -> l.getShardRouting().shardId())); + for (List shardStats : byShardId.values()) { + Set expectedLeaseIds = shardStats.stream() + .map(s -> ReplicationTracker.getPeerRecoveryRetentionLeaseId(s.getShardRouting())).collect(Collectors.toSet()); + for (ShardStats shardStat : shardStats) { + Set actualLeaseIds = shardStat.getRetentionLeaseStats().retentionLeases().leases().stream() + .map(RetentionLease::id).collect(Collectors.toSet()); + assertThat(expectedLeaseIds, everyItem(in(actualLeaseIds))); + } + } + } + }); + } + class RecoveryCorruption implements StubbableTransport.SendRequestBehavior { private final CountDownLatch corruptionCount; From 8faf2773e7b123d9ac7dea0d764eac0d74943ded Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 26 Dec 2019 09:02:02 -0500 Subject: [PATCH 338/686] Always use soft-deletes in InternalEngine (#50415) Peer recoveries become faster and use less storage (i.e., no more extra translog) with soft-deletes. Soft-deletes has been enabled by default since 7.0. We should make it mandatory in 8.0, so we can simplify the logic in the engine, recoveries, and other components. With this change, InternalEngine will always use soft-deletes regardless of the soft_deletes settings. --- .../index/engine/InternalEngine.java | 139 ++---- .../index/engine/ReadOnlyEngine.java | 10 +- .../index/mapper/SourceFieldMapper.java | 2 +- .../index/engine/InternalEngineTests.java | 439 ++++-------------- .../index/shard/IndexShardTests.java | 32 +- .../indices/stats/IndexStatsIT.java | 4 +- .../index/engine/EngineTestCase.java | 11 +- 7 files changed, 159 insertions(+), 478 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index 1e445c0b0ac85..a090501ca0de4 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -22,7 +22,6 @@ import org.apache.logging.log4j.Logger; import org.apache.lucene.codecs.blocktree.BlockTreeTermsReader; import org.apache.lucene.codecs.blocktree.BlockTreeTermsReader.FSTLoadMode; -import org.apache.lucene.document.Field; import org.apache.lucene.document.LongPoint; import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.index.DirectoryReader; @@ -170,7 +169,6 @@ public class InternalEngine extends Engine { private final CounterMetric numDocAppends = new CounterMetric(); private final CounterMetric numDocUpdates = new CounterMetric(); private final NumericDocValuesField softDeletesField = Lucene.newSoftDeletesField(); - private final boolean softDeleteEnabled; private final SoftDeletesPolicy softDeletesPolicy; private final LastRefreshedCheckpointListener lastRefreshedCheckpointListener; @@ -217,7 +215,6 @@ public InternalEngine(EngineConfig engineConfig) { }); assert translog.getGeneration() != null; this.translog = translog; - this.softDeleteEnabled = engineConfig.getIndexSettings().isSoftDeleteEnabled(); this.softDeletesPolicy = newSoftDeletesPolicy(); this.combinedDeletionPolicy = new CombinedDeletionPolicy(logger, translogDeletionPolicy, softDeletesPolicy, translog::getLastSyncedGlobalCheckpoint); @@ -254,7 +251,7 @@ public InternalEngine(EngineConfig engineConfig) { this.lastRefreshedCheckpointListener = new LastRefreshedCheckpointListener(localCheckpointTracker.getProcessedCheckpoint()); this.internalReaderManager.addListener(lastRefreshedCheckpointListener); maxSeqNoOfUpdatesOrDeletes = new AtomicLong(SequenceNumbers.max(localCheckpointTracker.getMaxSeqNo(), translog.getMaxSeqNo())); - if (softDeleteEnabled && localCheckpointTracker.getPersistedCheckpoint() < localCheckpointTracker.getMaxSeqNo()) { + if (localCheckpointTracker.getPersistedCheckpoint() < localCheckpointTracker.getMaxSeqNo()) { try (Searcher searcher = acquireSearcher("restore_version_map_and_checkpoint_tracker", SearcherScope.INTERNAL)) { restoreVersionMapAndCheckpointTracker(Lucene.wrapAllDocsLive(searcher.getDirectoryReader())); @@ -537,7 +534,6 @@ public void syncTranslog() throws IOException { public Translog.Snapshot readHistoryOperations(String reason, HistorySource historySource, MapperService mapperService, long startingSeqNo) throws IOException { if (historySource == HistorySource.INDEX) { - ensureSoftDeletesEnabled(); return newChangesSnapshot(reason, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false); } else { return getTranslog().newSnapshotFromMinSeqNo(startingSeqNo); @@ -551,7 +547,6 @@ public Translog.Snapshot readHistoryOperations(String reason, HistorySource hist public int estimateNumberOfHistoryOperations(String reason, HistorySource historySource, MapperService mapperService, long startingSeqNo) throws IOException { if (historySource == HistorySource.INDEX) { - ensureSoftDeletesEnabled(); try (Translog.Snapshot snapshot = newChangesSnapshot(reason, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false)) { return snapshot.totalOperations(); @@ -747,7 +742,7 @@ private OpVsLuceneDocStatus compareOpToLuceneDocBasedOnSeqNo(final Operation op) } else if (op.seqNo() > docAndSeqNo.seqNo) { status = OpVsLuceneDocStatus.OP_NEWER; } else if (op.seqNo() == docAndSeqNo.seqNo) { - assert localCheckpointTracker.hasProcessed(op.seqNo()) || softDeleteEnabled == false : + assert localCheckpointTracker.hasProcessed(op.seqNo()) : "local checkpoint tracker is not updated seq_no=" + op.seqNo() + " id=" + op.id(); status = OpVsLuceneDocStatus.OP_STALE_OR_EQUAL; } else { @@ -999,7 +994,7 @@ protected final IndexingStrategy planIndexingAsNonPrimary(Index index) throws IO versionMap.enforceSafeAccess(); final OpVsLuceneDocStatus opVsLucene = compareOpToLuceneDocBasedOnSeqNo(index); if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { - plan = IndexingStrategy.processAsStaleOp(softDeleteEnabled, index.version()); + plan = IndexingStrategy.processAsStaleOp(index.version()); } else { plan = IndexingStrategy.processNormally(opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, index.version()); } @@ -1151,7 +1146,6 @@ private void addDocs(final List docs, final IndexWriter i } private void addStaleDocs(final List docs, final IndexWriter indexWriter) throws IOException { - assert softDeleteEnabled : "Add history documents but soft-deletes is disabled"; for (ParseContext.Document doc : docs) { doc.add(softDeletesField); // soft-deleted every document before adding to Lucene } @@ -1212,9 +1206,8 @@ public static IndexingStrategy processButSkipLucene(boolean currentNotFoundOrDel false, versionForIndexing, null); } - static IndexingStrategy processAsStaleOp(boolean addStaleOpToLucene, long versionForIndexing) { - return new IndexingStrategy(false, false, false, - addStaleOpToLucene, versionForIndexing, null); + static IndexingStrategy processAsStaleOp(long versionForIndexing) { + return new IndexingStrategy(false, false, false, true, versionForIndexing, null); } } @@ -1243,18 +1236,10 @@ private boolean assertDocDoesNotExist(final Index index, final boolean allowDele } private void updateDocs(final Term uid, final List docs, final IndexWriter indexWriter) throws IOException { - if (softDeleteEnabled) { - if (docs.size() > 1) { - indexWriter.softUpdateDocuments(uid, docs, softDeletesField); - } else { - indexWriter.softUpdateDocument(uid, docs.get(0), softDeletesField); - } + if (docs.size() > 1) { + indexWriter.softUpdateDocuments(uid, docs, softDeletesField); } else { - if (docs.size() > 1) { - indexWriter.updateDocuments(uid, docs); - } else { - indexWriter.updateDocument(uid, docs.get(0)); - } + indexWriter.softUpdateDocument(uid, docs.get(0), softDeletesField); } numDocUpdates.inc(docs.size()); } @@ -1293,6 +1278,12 @@ public DeleteResult delete(Delete delete) throws IOException { deleteResult = new DeleteResult( plan.versionOfDeletion, delete.primaryTerm(), delete.seqNo(), plan.currentlyDeleted == false); } + if (plan.deleteFromLucene) { + numDocDeletes.inc(); + versionMap.putDeleteUnderLock(delete.uid().bytes(), + new DeleteVersionValue(plan.versionOfDeletion, delete.seqNo(), delete.primaryTerm(), + engineConfig.getThreadPool().relativeTimeInMillis())); + } } if (delete.origin().isFromTranslog() == false && deleteResult.getResultType() == Result.Type.SUCCESS) { final Translog.Location location = translog.add(new Translog.Delete(delete, deleteResult)); @@ -1342,7 +1333,7 @@ protected final DeletionStrategy planDeletionAsNonPrimary(Delete delete) throws } else { final OpVsLuceneDocStatus opVsLucene = compareOpToLuceneDocBasedOnSeqNo(delete); if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { - plan = DeletionStrategy.processAsStaleOp(softDeleteEnabled, delete.version()); + plan = DeletionStrategy.processAsStaleOp(delete.version()); } else { plan = DeletionStrategy.processNormally(opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, delete.version()); } @@ -1392,30 +1383,18 @@ private DeletionStrategy planDeletionAsPrimary(Delete delete) throws IOException private DeleteResult deleteInLucene(Delete delete, DeletionStrategy plan) throws IOException { assert assertMaxSeqNoOfUpdatesIsAdvanced(delete.uid(), delete.seqNo(), false, false); try { - if (softDeleteEnabled) { - final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newDeleteTombstoneDoc(delete.id()); - assert tombstone.docs().size() == 1 : "Tombstone doc should have single doc [" + tombstone + "]"; - tombstone.updateSeqID(delete.seqNo(), delete.primaryTerm()); - tombstone.version().setLongValue(plan.versionOfDeletion); - final ParseContext.Document doc = tombstone.docs().get(0); - assert doc.getField(SeqNoFieldMapper.TOMBSTONE_NAME) != null : - "Delete tombstone document but _tombstone field is not set [" + doc + " ]"; - doc.add(softDeletesField); - if (plan.addStaleOpToLucene || plan.currentlyDeleted) { - indexWriter.addDocument(doc); - } else { - indexWriter.softUpdateDocument(delete.uid(), doc, softDeletesField); - } - } else if (plan.currentlyDeleted == false) { - // any exception that comes from this is a either an ACE or a fatal exception there - // can't be any document failures coming from this - indexWriter.deleteDocuments(delete.uid()); - } - if (plan.deleteFromLucene) { - numDocDeletes.inc(); - versionMap.putDeleteUnderLock(delete.uid().bytes(), - new DeleteVersionValue(plan.versionOfDeletion, delete.seqNo(), delete.primaryTerm(), - engineConfig.getThreadPool().relativeTimeInMillis())); + final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newDeleteTombstoneDoc(delete.id()); + assert tombstone.docs().size() == 1 : "Tombstone doc should have single doc [" + tombstone + "]"; + tombstone.updateSeqID(delete.seqNo(), delete.primaryTerm()); + tombstone.version().setLongValue(plan.versionOfDeletion); + final ParseContext.Document doc = tombstone.docs().get(0); + assert doc.getField(SeqNoFieldMapper.TOMBSTONE_NAME) != null : + "Delete tombstone document but _tombstone field is not set [" + doc + " ]"; + doc.add(softDeletesField); + if (plan.addStaleOpToLucene || plan.currentlyDeleted) { + indexWriter.addDocument(doc); + } else { + indexWriter.softUpdateDocument(delete.uid(), doc, softDeletesField); } return new DeleteResult( plan.versionOfDeletion, delete.primaryTerm(), delete.seqNo(), plan.currentlyDeleted == false); @@ -1475,8 +1454,8 @@ public static DeletionStrategy processButSkipLucene(boolean currentlyDeleted, lo return new DeletionStrategy(false, false, currentlyDeleted, versionOfDeletion, null); } - static DeletionStrategy processAsStaleOp(boolean addStaleOpToLucene, long versionOfDeletion) { - return new DeletionStrategy(false, addStaleOpToLucene, false, versionOfDeletion, null); + static DeletionStrategy processAsStaleOp(long versionOfDeletion) { + return new DeletionStrategy(false, true, false, versionOfDeletion, null); } } @@ -1519,7 +1498,7 @@ private NoOpResult innerNoOp(final NoOp noOp) throws IOException { SequenceNumbers.UNASSIGNED_SEQ_NO, preFlightError.get()); } else { markSeqNoAsSeen(noOp.seqNo()); - if (softDeleteEnabled && hasBeenProcessedBefore(noOp) == false) { + if (hasBeenProcessedBefore(noOp) == false) { try { final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newNoopTombstoneDoc(noOp.reason()); tombstone.updateSeqID(noOp.seqNo(), noOp.primaryTerm()); @@ -2236,11 +2215,9 @@ private IndexWriterConfig getIndexWriterConfig() { MergePolicy mergePolicy = config().getMergePolicy(); // always configure soft-deletes field so an engine with soft-deletes disabled can open a Lucene index with soft-deletes. iwc.setSoftDeletesField(Lucene.SOFT_DELETES_FIELD); - if (softDeleteEnabled) { - mergePolicy = new RecoverySourcePruneMergePolicy(SourceFieldMapper.RECOVERY_SOURCE_NAME, softDeletesPolicy::getRetentionQuery, - new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, softDeletesPolicy::getRetentionQuery, - new PrunePostingsMergePolicy(mergePolicy, IdFieldMapper.NAME))); - } + mergePolicy = new RecoverySourcePruneMergePolicy(SourceFieldMapper.RECOVERY_SOURCE_NAME, softDeletesPolicy::getRetentionQuery, + new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, softDeletesPolicy::getRetentionQuery, + new PrunePostingsMergePolicy(mergePolicy, IdFieldMapper.NAME))); boolean shuffleForcedMerge = Booleans.parseBoolean(System.getProperty("es.shuffle_forced_merge", Boolean.TRUE.toString())); if (shuffleForcedMerge) { // We wrap the merge policy for all indices even though it is mostly useful for time-based indices @@ -2441,9 +2418,7 @@ protected void commitIndexWriter(final IndexWriter writer, final Translog transl commitData.put(SequenceNumbers.MAX_SEQ_NO, Long.toString(localCheckpointTracker.getMaxSeqNo())); commitData.put(MAX_UNSAFE_AUTO_ID_TIMESTAMP_COMMIT_ID, Long.toString(maxUnsafeAutoIdTimestamp.get())); commitData.put(HISTORY_UUID_KEY, historyUUID); - if (softDeleteEnabled) { - commitData.put(Engine.MIN_RETAINED_SEQNO, Long.toString(softDeletesPolicy.getMinRetainedSeqNo())); - } + commitData.put(Engine.MIN_RETAINED_SEQNO, Long.toString(softDeletesPolicy.getMinRetainedSeqNo())); logger.trace("committing writer with commit data [{}]", commitData); return commitData.entrySet().iterator(); }); @@ -2600,17 +2575,9 @@ long getNumDocUpdates() { return numDocUpdates.count(); } - private void ensureSoftDeletesEnabled() { - if (softDeleteEnabled == false) { - assert false : "index " + shardId.getIndex() + " does not have soft-deletes enabled"; - throw new IllegalStateException("index " + shardId.getIndex() + " does not have soft-deletes enabled"); - } - } - @Override public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperService, long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException { - ensureSoftDeletesEnabled(); ensureOpen(); refreshIfNeeded(source, toSeqNo); Searcher searcher = acquireSearcher(source, SearcherScope.INTERNAL); @@ -2635,7 +2602,6 @@ public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperS public boolean hasCompleteOperationHistory(String reason, HistorySource historySource, MapperService mapperService, long startingSeqNo) throws IOException { if (historySource == HistorySource.INDEX) { - ensureSoftDeletesEnabled(); return getMinRetainedSeqNo() <= startingSeqNo; } else { final long currentLocalCheckpoint = localCheckpointTracker.getProcessedCheckpoint(); @@ -2661,14 +2627,12 @@ public boolean hasCompleteOperationHistory(String reason, HistorySource historyS * Operations whose seq# are at least this value should exist in the Lucene index. */ public final long getMinRetainedSeqNo() { - ensureSoftDeletesEnabled(); return softDeletesPolicy.getMinRetainedSeqNo(); } @Override public Closeable acquireHistoryRetentionLock(HistorySource historySource) { if (historySource == HistorySource.INDEX) { - ensureSoftDeletesEnabled(); return softDeletesPolicy.acquireRetentionLock(); } else { return translog.acquireRetentionLock(); @@ -2686,40 +2650,29 @@ private static Map commitDataAsMap(final IndexWriter indexWriter return commitData; } - private final class AssertingIndexWriter extends IndexWriter { + private static class AssertingIndexWriter extends IndexWriter { AssertingIndexWriter(Directory d, IndexWriterConfig conf) throws IOException { super(d, conf); } + @Override - public long updateDocument(Term term, Iterable doc) throws IOException { - assert softDeleteEnabled == false : "Call #updateDocument but soft-deletes is enabled"; - return super.updateDocument(term, doc); - } - @Override - public long updateDocuments(Term delTerm, Iterable> docs) throws IOException { - assert softDeleteEnabled == false : "Call #updateDocuments but soft-deletes is enabled"; - return super.updateDocuments(delTerm, docs); - } - @Override - public long deleteDocuments(Term... terms) throws IOException { - assert softDeleteEnabled == false : "Call #deleteDocuments but soft-deletes is enabled"; - return super.deleteDocuments(terms); + public long updateDocument(Term term, Iterable doc) { + throw new AssertionError("must not hard update document"); } + @Override - public long softUpdateDocument(Term term, Iterable doc, Field... softDeletes) throws IOException { - assert softDeleteEnabled : "Call #softUpdateDocument but soft-deletes is disabled"; - return super.softUpdateDocument(term, doc, softDeletes); + public long updateDocuments(Term delTerm, Iterable> docs) { + throw new AssertionError("must not hard update documents"); } + @Override - public long softUpdateDocuments(Term term, Iterable> docs, - Field... softDeletes) throws IOException { - assert softDeleteEnabled : "Call #softUpdateDocuments but soft-deletes is disabled"; - return super.softUpdateDocuments(term, docs, softDeletes); + public long deleteDocuments(Term... terms) { + throw new AssertionError("must not hard delete documents"); } + @Override public long tryDeleteDocument(IndexReader readerIn, int docID) { - assert false : "#tryDeleteDocument is not supported. See Lucene#DirectoryReaderWithAllLiveDocs"; - throw new UnsupportedOperationException(); + throw new AssertionError("tryDeleteDocument is not supported. See Lucene#DirectoryReaderWithAllLiveDocs"); } } diff --git a/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java b/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java index 768655cf1c852..4bc06aeea00b9 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java @@ -168,15 +168,12 @@ public void verifyEngineBeforeIndexClosing() throws IllegalStateException { protected final ElasticsearchDirectoryReader wrapReader(DirectoryReader reader, Function readerWrapperFunction) throws IOException { - if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { - reader = new SoftDeletesDirectoryReaderWrapper(reader, Lucene.SOFT_DELETES_FIELD); - } reader = readerWrapperFunction.apply(reader); return ElasticsearchDirectoryReader.wrap(reader, engineConfig.getShardId()); } protected DirectoryReader open(IndexCommit commit) throws IOException { - return DirectoryReader.open(commit, OFF_HEAP_READER_ATTRIBUTES); + return new SoftDeletesDirectoryReaderWrapper(DirectoryReader.open(commit, OFF_HEAP_READER_ATTRIBUTES), Lucene.SOFT_DELETES_FIELD); } private DocsStats docsStats(final SegmentInfos lastCommittedSegmentInfos) { @@ -313,10 +310,7 @@ public Closeable acquireHistoryRetentionLock(HistorySource historySource) { @Override public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperService, long fromSeqNo, long toSeqNo, - boolean requiredFullRange) throws IOException { - if (engineConfig.getIndexSettings().isSoftDeleteEnabled() == false) { - throw new IllegalStateException("accessing changes snapshot requires soft-deletes enabled"); - } + boolean requiredFullRange) { return newEmptySnapshot(); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java index 4f5e1b79bd1f1..2b0bbe40a2fb3 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java @@ -236,7 +236,7 @@ protected void parseCreateField(ParseContext context, List field fields.add(new StoredField(fieldType().name(), ref.bytes, ref.offset, ref.length)); } - if (originalSource != null && adaptedSource != originalSource && context.indexSettings().isSoftDeleteEnabled()) { + if (originalSource != null && adaptedSource != originalSource) { // if we omitted source or modified it we add the _recovery_source to ensure we have it for ops based recovery BytesRef ref = originalSource.toBytesRef(); fields.add(new StoredField(RECOVERY_SOURCE_NAME, ref.bytes, ref.offset, ref.length)); diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 79e5cf7489ca3..9a2f79b7530a7 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -175,6 +175,7 @@ import java.util.function.ToLongBiFunction; import java.util.stream.Collectors; import java.util.stream.LongStream; +import java.util.stream.StreamSupport; import static java.util.Collections.shuffle; import static org.elasticsearch.index.engine.Engine.Operation.Origin.LOCAL_RESET; @@ -277,198 +278,6 @@ public void testVersionMapAfterAutoIDDocument() throws IOException { } } - public void testSegmentsWithoutSoftDeletes() throws Exception { - Settings settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); - IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); - try (Store store = createStore(); - InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { - List segments = engine.segments(false); - assertThat(segments.isEmpty(), equalTo(true)); - assertThat(engine.segmentsStats(false, false).getCount(), equalTo(0L)); - assertThat(engine.segmentsStats(false, false).getMemoryInBytes(), equalTo(0L)); - - // create two docs and refresh - ParsedDocument doc = testParsedDocument("1", null, testDocumentWithTextField(), B_1, null); - Engine.Index first = indexForDoc(doc); - Engine.IndexResult firstResult = engine.index(first); - ParsedDocument doc2 = testParsedDocument("2", null, testDocumentWithTextField(), B_2, null); - Engine.Index second = indexForDoc(doc2); - Engine.IndexResult secondResult = engine.index(second); - assertThat(secondResult.getTranslogLocation(), greaterThan(firstResult.getTranslogLocation())); - engine.refresh("test"); - - segments = engine.segments(false); - assertThat(segments.size(), equalTo(1)); - SegmentsStats stats = engine.segmentsStats(false, false); - assertThat(stats.getCount(), equalTo(1L)); - assertThat(stats.getTermsMemoryInBytes(), greaterThan(0L)); - assertThat(stats.getStoredFieldsMemoryInBytes(), greaterThan(0L)); - assertThat(stats.getTermVectorsMemoryInBytes(), equalTo(0L)); - assertThat(stats.getNormsMemoryInBytes(), greaterThan(0L)); - assertThat(stats.getDocValuesMemoryInBytes(), greaterThan(0L)); - assertThat(segments.get(0).isCommitted(), equalTo(false)); - assertThat(segments.get(0).isSearch(), equalTo(true)); - assertThat(segments.get(0).getNumDocs(), equalTo(2)); - assertThat(segments.get(0).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(0).isCompound(), equalTo(true)); - assertThat(segments.get(0).ramTree, nullValue()); - engine.flush(); - - segments = engine.segments(false); - assertThat(segments.size(), equalTo(1)); - assertThat(engine.segmentsStats(false, false).getCount(), equalTo(1L)); - assertThat(segments.get(0).isCommitted(), equalTo(true)); - assertThat(segments.get(0).isSearch(), equalTo(true)); - assertThat(segments.get(0).getNumDocs(), equalTo(2)); - assertThat(segments.get(0).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(0).isCompound(), equalTo(true)); - - ParsedDocument doc3 = testParsedDocument("3", null, testDocumentWithTextField(), B_3, null); - engine.index(indexForDoc(doc3)); - engine.refresh("test"); - - segments = engine.segments(false); - assertThat(segments.size(), equalTo(2)); - assertThat(engine.segmentsStats(false, false).getCount(), equalTo(2L)); - assertThat(engine.segmentsStats(false, false).getTermsMemoryInBytes(), - greaterThan(stats.getTermsMemoryInBytes())); - assertThat(engine.segmentsStats(false, false).getStoredFieldsMemoryInBytes(), - greaterThan(stats.getStoredFieldsMemoryInBytes())); - assertThat(engine.segmentsStats(false, false).getTermVectorsMemoryInBytes(), equalTo(0L)); - assertThat(engine.segmentsStats(false, false).getNormsMemoryInBytes(), - greaterThan(stats.getNormsMemoryInBytes())); - assertThat(engine.segmentsStats(false, false).getDocValuesMemoryInBytes(), - greaterThan(stats.getDocValuesMemoryInBytes())); - assertThat(segments.get(0).getGeneration() < segments.get(1).getGeneration(), equalTo(true)); - assertThat(segments.get(0).isCommitted(), equalTo(true)); - assertThat(segments.get(0).isSearch(), equalTo(true)); - assertThat(segments.get(0).getNumDocs(), equalTo(2)); - assertThat(segments.get(0).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(0).isCompound(), equalTo(true)); - - - assertThat(segments.get(1).isCommitted(), equalTo(false)); - assertThat(segments.get(1).isSearch(), equalTo(true)); - assertThat(segments.get(1).getNumDocs(), equalTo(1)); - assertThat(segments.get(1).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(1).isCompound(), equalTo(true)); - - - engine.delete(new Engine.Delete("1", newUid(doc), primaryTerm.get())); - engine.refresh("test"); - - segments = engine.segments(false); - assertThat(segments.size(), equalTo(2)); - assertThat(engine.segmentsStats(false, false).getCount(), equalTo(2L)); - assertThat(segments.get(0).getGeneration() < segments.get(1).getGeneration(), equalTo(true)); - assertThat(segments.get(0).isCommitted(), equalTo(true)); - assertThat(segments.get(0).isSearch(), equalTo(true)); - assertThat(segments.get(0).getNumDocs(), equalTo(1)); - assertThat(segments.get(0).getDeletedDocs(), equalTo(1)); - assertThat(segments.get(0).isCompound(), equalTo(true)); - - assertThat(segments.get(1).isCommitted(), equalTo(false)); - assertThat(segments.get(1).isSearch(), equalTo(true)); - assertThat(segments.get(1).getNumDocs(), equalTo(1)); - assertThat(segments.get(1).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(1).isCompound(), equalTo(true)); - - engine.onSettingsChanged(indexSettings.getTranslogRetentionAge(), indexSettings.getTranslogRetentionSize(), - indexSettings.getSoftDeleteRetentionOperations()); - ParsedDocument doc4 = testParsedDocument("4", null, testDocumentWithTextField(), B_3, null); - engine.index(indexForDoc(doc4)); - engine.refresh("test"); - - segments = engine.segments(false); - assertThat(segments.size(), equalTo(3)); - assertThat(engine.segmentsStats(false, false).getCount(), equalTo(3L)); - assertThat(segments.get(0).getGeneration() < segments.get(1).getGeneration(), equalTo(true)); - assertThat(segments.get(0).isCommitted(), equalTo(true)); - assertThat(segments.get(0).isSearch(), equalTo(true)); - assertThat(segments.get(0).getNumDocs(), equalTo(1)); - assertThat(segments.get(0).getDeletedDocs(), equalTo(1)); - assertThat(segments.get(0).isCompound(), equalTo(true)); - - assertThat(segments.get(1).isCommitted(), equalTo(false)); - assertThat(segments.get(1).isSearch(), equalTo(true)); - assertThat(segments.get(1).getNumDocs(), equalTo(1)); - assertThat(segments.get(1).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(1).isCompound(), equalTo(true)); - - assertThat(segments.get(2).isCommitted(), equalTo(false)); - assertThat(segments.get(2).isSearch(), equalTo(true)); - assertThat(segments.get(2).getNumDocs(), equalTo(1)); - assertThat(segments.get(2).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(2).isCompound(), equalTo(true)); - - // internal refresh - lets make sure we see those segments in the stats - ParsedDocument doc5 = testParsedDocument("5", null, testDocumentWithTextField(), B_3, null); - engine.index(indexForDoc(doc5)); - engine.refresh("test", Engine.SearcherScope.INTERNAL, true); - - segments = engine.segments(false); - assertThat(segments.size(), equalTo(4)); - assertThat(engine.segmentsStats(false, false).getCount(), equalTo(4L)); - assertThat(segments.get(0).getGeneration() < segments.get(1).getGeneration(), equalTo(true)); - assertThat(segments.get(0).isCommitted(), equalTo(true)); - assertThat(segments.get(0).isSearch(), equalTo(true)); - assertThat(segments.get(0).getNumDocs(), equalTo(1)); - assertThat(segments.get(0).getDeletedDocs(), equalTo(1)); - assertThat(segments.get(0).isCompound(), equalTo(true)); - - assertThat(segments.get(1).isCommitted(), equalTo(false)); - assertThat(segments.get(1).isSearch(), equalTo(true)); - assertThat(segments.get(1).getNumDocs(), equalTo(1)); - assertThat(segments.get(1).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(1).isCompound(), equalTo(true)); - - assertThat(segments.get(2).isCommitted(), equalTo(false)); - assertThat(segments.get(2).isSearch(), equalTo(true)); - assertThat(segments.get(2).getNumDocs(), equalTo(1)); - assertThat(segments.get(2).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(2).isCompound(), equalTo(true)); - - assertThat(segments.get(3).isCommitted(), equalTo(false)); - assertThat(segments.get(3).isSearch(), equalTo(false)); - assertThat(segments.get(3).getNumDocs(), equalTo(1)); - assertThat(segments.get(3).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(3).isCompound(), equalTo(true)); - - // now refresh the external searcher and make sure it has the new segment - engine.refresh("test"); - segments = engine.segments(false); - assertThat(segments.size(), equalTo(4)); - assertThat(engine.segmentsStats(false, false).getCount(), equalTo(4L)); - assertThat(segments.get(0).getGeneration() < segments.get(1).getGeneration(), equalTo(true)); - assertThat(segments.get(0).isCommitted(), equalTo(true)); - assertThat(segments.get(0).isSearch(), equalTo(true)); - assertThat(segments.get(0).getNumDocs(), equalTo(1)); - assertThat(segments.get(0).getDeletedDocs(), equalTo(1)); - assertThat(segments.get(0).isCompound(), equalTo(true)); - - assertThat(segments.get(1).isCommitted(), equalTo(false)); - assertThat(segments.get(1).isSearch(), equalTo(true)); - assertThat(segments.get(1).getNumDocs(), equalTo(1)); - assertThat(segments.get(1).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(1).isCompound(), equalTo(true)); - - assertThat(segments.get(2).isCommitted(), equalTo(false)); - assertThat(segments.get(2).isSearch(), equalTo(true)); - assertThat(segments.get(2).getNumDocs(), equalTo(1)); - assertThat(segments.get(2).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(2).isCompound(), equalTo(true)); - - assertThat(segments.get(3).isCommitted(), equalTo(false)); - assertThat(segments.get(3).isSearch(), equalTo(true)); - assertThat(segments.get(3).getNumDocs(), equalTo(1)); - assertThat(segments.get(3).getDeletedDocs(), equalTo(0)); - assertThat(segments.get(3).isCompound(), equalTo(true)); - } - } - public void testVerboseSegments() throws Exception { try (Store store = createStore(); Engine engine = createEngine(defaultSettings, store, createTempDir(), NoMergePolicy.INSTANCE)) { @@ -606,15 +415,10 @@ public void testSegmentsStatsIncludingFileSizes() throws Exception { } } - public void testSegmentsWithSoftDeletes() throws Exception { - Settings.Builder settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true); - final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); + public void testSegments() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); try (Store store = createStore(); - InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null, + InternalEngine engine = createEngine(config(defaultSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null, null, globalCheckpoint::get))) { assertThat(engine.segments(false), empty()); int numDocsFirstSegment = randomIntBetween(5, 50); @@ -1457,55 +1261,6 @@ public void testVersioningNewIndex() throws IOException { assertThat(indexResult.getVersion(), equalTo(1L)); } - public void testForceMergeWithoutSoftDeletes() throws IOException { - Settings settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); - IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); - try (Store store = createStore(); - Engine engine = createEngine(config(IndexSettingsModule.newIndexSettings(indexMetaData), store, createTempDir(), - new LogByteSizeMergePolicy(), null))) { // use log MP here we test some behavior in ESMP - int numDocs = randomIntBetween(10, 100); - for (int i = 0; i < numDocs; i++) { - ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null); - Engine.Index index = indexForDoc(doc); - engine.index(index); - engine.refresh("test"); - } - try (Engine.Searcher test = engine.acquireSearcher("test")) { - assertEquals(numDocs, test.getIndexReader().numDocs()); - } - engine.forceMerge(true, 1, false, false, false); - engine.refresh("test"); - assertEquals(engine.segments(true).size(), 1); - - ParsedDocument doc = testParsedDocument(Integer.toString(0), null, testDocument(), B_1, null); - Engine.Index index = indexForDoc(doc); - engine.delete(new Engine.Delete(index.id(), index.uid(), primaryTerm.get())); - //expunge deletes - engine.forceMerge(true, -1, true, false, false); - engine.refresh("test"); - - assertEquals(engine.segments(true).size(), 1); - try (Engine.Searcher test = engine.acquireSearcher("test")) { - assertEquals(numDocs - 1, test.getIndexReader().numDocs()); - assertEquals(engine.config().getMergePolicy().toString(), numDocs - 1, test.getIndexReader().maxDoc()); - } - - doc = testParsedDocument(Integer.toString(1), null, testDocument(), B_1, null); - index = indexForDoc(doc); - engine.delete(new Engine.Delete(index.id(), index.uid(), primaryTerm.get())); - //expunge deletes - engine.forceMerge(true, 10, false, false, false); - engine.refresh("test"); - assertEquals(engine.segments(true).size(), 1); - try (Engine.Searcher test = engine.acquireSearcher("test")) { - assertEquals(numDocs - 2, test.getIndexReader().numDocs()); - assertEquals(numDocs - 1, test.getIndexReader().maxDoc()); - } - } - } - /* * we are testing an edge case here where we have a fully deleted segment that is retained but has all it's IDs pruned away. */ @@ -1535,16 +1290,10 @@ public void testLookupVersionWithPrunedAwayIds() throws IOException { } public void testUpdateWithFullyDeletedSegments() throws IOException { - Settings.Builder settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), Integer.MAX_VALUE); - final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); final Set liveDocs = new HashSet<>(); try (Store store = createStore(); - InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, + InternalEngine engine = createEngine(config(defaultSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get))) { int numDocs = scaledRandomIntBetween(10, 100); for (int i = 0; i < numDocs; i++) { @@ -1565,7 +1314,6 @@ public void testForceMergeWithSoftDeletesRetention() throws Exception { final long retainedExtraOps = randomLongBetween(0, 10); Settings.Builder settings = Settings.builder() .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), retainedExtraOps); final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); @@ -1639,7 +1387,6 @@ public void testForceMergeWithSoftDeletesRetentionAndRecoverySource() throws Exc final long retainedExtraOps = randomLongBetween(0, 10); Settings.Builder settings = Settings.builder() .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), retainedExtraOps); final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); @@ -4059,14 +3806,9 @@ public void testLookupSeqNoByIdInLucene() throws Exception { } } Randomness.shuffle(operations); - Settings.Builder settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true); - final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); Map latestOps = new HashMap<>(); // id -> latest seq_no try (Store store = createStore(); - InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null))) { + InternalEngine engine = createEngine(config(defaultSettings, store, createTempDir(), newMergePolicy(), null))) { CheckedRunnable lookupAndCheck = () -> { try (Engine.Searcher searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL)) { Map liveOps = latestOps.entrySet().stream() @@ -5325,7 +5067,6 @@ private void assertOperationHistoryInLucene(List operations) t Lucene.SOFT_DELETES_FIELD, () -> new MatchAllDocsQuery(), engine.config().getMergePolicy()); Settings.Builder settings = Settings.builder() .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); @@ -5363,7 +5104,6 @@ public void testKeepMinRetainedSeqNoByMergePolicy() throws IOException { IOUtils.close(engine, store); Settings.Builder settings = Settings.builder() .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); @@ -5488,11 +5228,8 @@ public void testLuceneSnapshotRefreshesOnlyOnce() throws Exception { final MapperService mapperService = createMapperService(); final long maxSeqNo = randomLongBetween(10, 50); final AtomicLong refreshCounter = new AtomicLong(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(Settings.builder(). - put(defaultSettings.getSettings()).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true)).build()); try (Store store = createStore(); - InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), + InternalEngine engine = createEngine(config(defaultSettings, store, createTempDir(), newMergePolicy(), null, new ReferenceManager.RefreshListener() { @Override @@ -5548,14 +5285,8 @@ public void testAcquireSearcherOnClosingEngine() throws Exception { public void testNoOpOnClosingEngine() throws Exception { engine.close(); - Settings settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true).build(); - IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); - assertTrue(indexSettings.isSoftDeleteEnabled()); try (Store store = createStore(); - InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { + InternalEngine engine = createEngine(config(defaultSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { engine.close(); expectThrows(AlreadyClosedException.class, () -> engine.noOp( new Engine.NoOp(2, primaryTerm.get(), LOCAL_TRANSLOG_RECOVERY, System.nanoTime(), "reason"))); @@ -5564,14 +5295,8 @@ public void testNoOpOnClosingEngine() throws Exception { public void testSoftDeleteOnClosingEngine() throws Exception { engine.close(); - Settings settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true).build(); - IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); - assertTrue(indexSettings.isSoftDeleteEnabled()); try (Store store = createStore(); - InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { + InternalEngine engine = createEngine(config(defaultSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { engine.close(); expectThrows(AlreadyClosedException.class, () -> engine.delete(replicaDeleteForDoc("test", 42, 7, System.nanoTime()))); } @@ -5605,19 +5330,13 @@ public void testTrackMaxSeqNoOfUpdatesOrDeletesOnPrimary() throws Exception { } public void testRebuildLocalCheckpointTrackerAndVersionMap() throws Exception { - Settings.Builder settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 10000) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true); - final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); Path translogPath = createTempDir(); List operations = generateHistoryOnReplica(between(1, 500), randomBoolean(), randomBoolean(), randomBoolean()); List> commits = new ArrayList<>(); commits.add(new ArrayList<>()); try (Store store = createStore()) { - EngineConfig config = config(indexSettings, store, translogPath, NoMergePolicy.INSTANCE, null, null, globalCheckpoint::get); + EngineConfig config = config(defaultSettings, store, translogPath, NoMergePolicy.INSTANCE, null, null, globalCheckpoint::get); final List docs; try (InternalEngine engine = createEngine(config)) { List flushedOperations = new ArrayList<>(); @@ -5680,42 +5399,79 @@ public void testRebuildLocalCheckpointTrackerAndVersionMap() throws Exception { } } - public void testOpenSoftDeletesIndexWithSoftDeletesDisabled() throws Exception { - try (Store store = createStore()) { - Path translogPath = createTempDir(); - final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); - final IndexSettings softDeletesEnabled = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(Settings.builder(). - put(defaultSettings.getSettings()).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true)).build()); - final List docs; - try (InternalEngine engine = createEngine( - config(softDeletesEnabled, store, translogPath, newMergePolicy(), null, null, globalCheckpoint::get))) { - List ops = generateHistoryOnReplica(between(1, 100), randomBoolean(), randomBoolean(), randomBoolean()); - applyOperations(engine, ops); - engine.syncTranslog(); // to advance persisted checkpoint - globalCheckpoint.set(randomLongBetween(globalCheckpoint.get(), engine.getPersistedLocalCheckpoint())); - engine.flush(); - docs = getDocIds(engine, true); + public void testRecoverFromHardDeletesIndex() throws Exception { + IndexWriterFactory hardDeletesWriter = (directory, iwc) -> new IndexWriter(directory, iwc) { + boolean isTombstone(Iterable doc) { + return StreamSupport.stream(doc.spliterator(), false).anyMatch(d -> d.name().equals(Lucene.SOFT_DELETES_FIELD)); } - final IndexSettings softDeletesDisabled = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(Settings.builder() - .put(defaultSettings.getSettings()).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false)).build()); - EngineConfig config = config(softDeletesDisabled, store, translogPath, newMergePolicy(), null, null, globalCheckpoint::get); - try (InternalEngine engine = createEngine(config)) { - assertThat(getDocIds(engine, true), equalTo(docs)); + + @Override + public long addDocument(Iterable doc) throws IOException { + if (isTombstone(doc)) { + return 0; + } + return super.addDocument(doc); } - } - } - public void testRequireSoftDeletesWhenAccessingChangesSnapshot() throws Exception { + @Override + public long addDocuments(Iterable> docs) throws IOException { + if (StreamSupport.stream(docs.spliterator(), false).anyMatch(this::isTombstone)) { + return 0; + } + return super.addDocuments(docs); + } + + @Override + public long softUpdateDocument(Term term, Iterable doc, + Field... softDeletes) throws IOException { + if (isTombstone(doc)) { + return super.deleteDocuments(term); + } else { + return super.updateDocument(term, doc); + } + } + + @Override + public long softUpdateDocuments(Term term, Iterable> docs, + Field... softDeletes) throws IOException { + if (StreamSupport.stream(docs.spliterator(), false).anyMatch(this::isTombstone)) { + return super.deleteDocuments(term); + } else { + return super.updateDocuments(term, docs); + } + } + }; + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + Path translogPath = createTempDir(); + List operations = generateHistoryOnReplica(between(1, 500), randomBoolean(), randomBoolean(), randomBoolean()); try (Store store = createStore()) { - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(Settings.builder(). - put(defaultSettings.getSettings()).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false)).build()); - try (InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null))) { - AssertionError error = expectThrows(AssertionError.class, - () -> engine.newChangesSnapshot("test", createMapperService(), 0, randomNonNegativeLong(), randomBoolean())); - assertThat(error.getMessage(), containsString("does not have soft-deletes enabled")); + EngineConfig config = config(defaultSettings, store, translogPath, NoMergePolicy.INSTANCE, null, null, globalCheckpoint::get); + final List docs; + try (InternalEngine hardDeletesEngine = createEngine(defaultSettings, store, translogPath, newMergePolicy(), + hardDeletesWriter, null, globalCheckpoint::get)) { + for (Engine.Operation op : operations) { + applyOperation(hardDeletesEngine, op); + if (randomBoolean()) { + hardDeletesEngine.syncTranslog(); + globalCheckpoint.set(randomLongBetween(globalCheckpoint.get(), hardDeletesEngine.getPersistedLocalCheckpoint())); + } + if (randomInt(100) < 10) { + hardDeletesEngine.refresh("test"); + } + if (randomInt(100) < 5) { + hardDeletesEngine.flush(true, true); + } + } + docs = getDocIds(hardDeletesEngine, true); + } + try (InternalEngine softDeletesEngine = new InternalEngine(config)) { // do not recover from translog + assertThat(softDeletesEngine.getVersionMap().keySet(), empty()); + softDeletesEngine.recoverFromTranslog(translogHandler, Long.MAX_VALUE); + if (randomBoolean()) { + softDeletesEngine.forceMerge(randomBoolean()); + } + assertThat(getDocIds(softDeletesEngine, true), equalTo(docs)); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(softDeletesEngine, createMapperService()); } } } @@ -5889,15 +5645,10 @@ public void testGetReaderAttributes() throws IOException { public void testPruneAwayDeletedButRetainedIds() throws Exception { IOUtils.close(engine, store); - Settings settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true).build(); - IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); - store = createStore(indexSettings, newDirectory()); + store = createStore(defaultSettings, newDirectory()); LogDocMergePolicy policy = new LogDocMergePolicy(); policy.setMinMergeDocs(10000); - try (InternalEngine engine = createEngine(indexSettings, store, createTempDir(), policy)) { + try (InternalEngine engine = createEngine(defaultSettings, store, createTempDir(), policy)) { int numDocs = between(1, 20); for (int i = 0; i < numDocs; i++) { index(engine, i); @@ -6040,11 +5791,6 @@ public void testAlwaysRecordReplicaOrPeerRecoveryOperationsToTranslog() throws E public void testNoOpFailure() throws IOException { engine.close(); - final Settings settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true).build(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); try (Store store = createStore(); Engine engine = createEngine((dir, iwc) -> new IndexWriter(dir, iwc) { @@ -6056,7 +5802,7 @@ public long addDocument(Iterable doc) throws IOExcepti }, null, null, - config(indexSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { + config(defaultSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { final Engine.NoOp op = new Engine.NoOp(0, 0, PRIMARY, System.currentTimeMillis(), "test"); final IllegalArgumentException e = expectThrows(IllegalArgumentException. class, () -> engine.noOp(op)); assertThat(e.getMessage(), equalTo("fatal")); @@ -6067,27 +5813,16 @@ public long addDocument(Iterable doc) throws IOExcepti } } - public void testDeleteFailureSoftDeletesEnabledDocAlreadyDeleted() throws IOException { - runTestDeleteFailure(true, InternalEngine::delete); - } - - public void testDeleteFailureSoftDeletesEnabled() throws IOException { - runTestDeleteFailure(true, (engine, op) -> {}); + public void testDeleteFailureDocAlreadyDeleted() throws IOException { + runTestDeleteFailure(InternalEngine::delete); } - public void testDeleteFailureSoftDeletesDisabled() throws IOException { - runTestDeleteFailure(false, (engine, op) -> {}); + public void testDeleteFailure() throws IOException { + runTestDeleteFailure((engine, op) -> {}); } - private void runTestDeleteFailure( - final boolean softDeletesEnabled, - final CheckedBiConsumer consumer) throws IOException { + private void runTestDeleteFailure(final CheckedBiConsumer consumer) throws IOException { engine.close(); - final Settings settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), softDeletesEnabled).build(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); final AtomicReference iw = new AtomicReference<>(); try (Store store = createStore(); InternalEngine engine = createEngine( @@ -6097,7 +5832,7 @@ private void runTestDeleteFailure( }, null, null, - config(indexSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { + config(defaultSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { engine.index(new Engine.Index(newUid("0"), primaryTerm.get(), InternalEngineTests.createParsedDoc("0", null))); final Engine.Delete op = new Engine.Delete("0", newUid("0"), primaryTerm.get()); consumer.accept(engine, op); diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 93867f4aa4c77..21d3c95c5942f 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -2911,25 +2911,23 @@ public void testDocStats() throws Exception { indexDoc(indexShard, "_doc", id); } // Need to update and sync the global checkpoint and the retention leases for the soft-deletes retention MergePolicy. - if (indexShard.indexSettings.isSoftDeleteEnabled()) { - final long newGlobalCheckpoint = indexShard.getLocalCheckpoint(); - if (indexShard.routingEntry().primary()) { - indexShard.updateLocalCheckpointForShard(indexShard.routingEntry().allocationId().getId(), - indexShard.getLocalCheckpoint()); - indexShard.updateGlobalCheckpointForShard(indexShard.routingEntry().allocationId().getId(), - indexShard.getLocalCheckpoint()); - indexShard.syncRetentionLeases(); - } else { - indexShard.updateGlobalCheckpointOnReplica(newGlobalCheckpoint, "test"); + final long newGlobalCheckpoint = indexShard.getLocalCheckpoint(); + if (indexShard.routingEntry().primary()) { + indexShard.updateLocalCheckpointForShard(indexShard.routingEntry().allocationId().getId(), + indexShard.getLocalCheckpoint()); + indexShard.updateGlobalCheckpointForShard(indexShard.routingEntry().allocationId().getId(), + indexShard.getLocalCheckpoint()); + indexShard.syncRetentionLeases(); + } else { + indexShard.updateGlobalCheckpointOnReplica(newGlobalCheckpoint, "test"); - final RetentionLeases retentionLeases = indexShard.getRetentionLeases(); - indexShard.updateRetentionLeasesOnReplica(new RetentionLeases( - retentionLeases.primaryTerm(), retentionLeases.version() + 1, - retentionLeases.leases().stream().map(lease -> new RetentionLease(lease.id(), newGlobalCheckpoint + 1, - lease.timestamp(), ReplicationTracker.PEER_RECOVERY_RETENTION_LEASE_SOURCE)).collect(Collectors.toList()))); - } - indexShard.sync(); + final RetentionLeases retentionLeases = indexShard.getRetentionLeases(); + indexShard.updateRetentionLeasesOnReplica(new RetentionLeases( + retentionLeases.primaryTerm(), retentionLeases.version() + 1, + retentionLeases.leases().stream().map(lease -> new RetentionLease(lease.id(), newGlobalCheckpoint + 1, + lease.timestamp(), ReplicationTracker.PEER_RECOVERY_RETENTION_LEASE_SOURCE)).collect(Collectors.toList()))); } + indexShard.sync(); // flush the buffered deletes final FlushRequest flushRequest = new FlushRequest(); flushRequest.force(false); diff --git a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java index 71d3cdb2ea610..a77091a2ce93e 100644 --- a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java +++ b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java @@ -982,9 +982,7 @@ public void testFilterCacheStats() throws Exception { indexRandom(false, true, client().prepareIndex("index").setId("1").setSource("foo", "bar"), client().prepareIndex("index").setId("2").setSource("foo", "baz")); - if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { - persistGlobalCheckpoint("index"); // Need to persist the global checkpoint for the soft-deletes retention MP. - } + persistGlobalCheckpoint("index"); // Need to persist the global checkpoint for the soft-deletes retention MP. refresh(); ensureGreen(); diff --git a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java index 2a676bb7b7750..81b2bdd8385ec 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java @@ -1067,8 +1067,7 @@ public static List readAllOperationsInLucene(Engine engine, * Asserts the provided engine has a consistent document history between translog and Lucene index. */ public static void assertConsistentHistoryBetweenTranslogAndLuceneIndex(Engine engine, MapperService mapper) throws IOException { - if (mapper == null || mapper.documentMapper() == null || engine.config().getIndexSettings().isSoftDeleteEnabled() == false - || (engine instanceof InternalEngine) == false) { + if (mapper == null || mapper.documentMapper() == null || (engine instanceof InternalEngine) == false) { return; } final List translogOps = new ArrayList<>(); @@ -1090,8 +1089,12 @@ public static void assertConsistentHistoryBetweenTranslogAndLuceneIndex(Engine e final long globalCheckpoint = EngineTestCase.getTranslog(engine).getLastSyncedGlobalCheckpoint(); final long retainedOps = engine.config().getIndexSettings().getSoftDeleteRetentionOperations(); final long seqNoForRecovery; - try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { - seqNoForRecovery = Long.parseLong(safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; + if (engine.config().getIndexSettings().isSoftDeleteEnabled()) { + try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { + seqNoForRecovery = Long.parseLong(safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; + } + } else { + seqNoForRecovery = engine.getMinRetainedSeqNo(); } final long minSeqNoToRetain = Math.min(seqNoForRecovery, globalCheckpoint + 1 - retainedOps); for (Translog.Operation translogOp : translogOps) { From 81631d2f6901f050aef7c53d0b89d6b663f8d16e Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Thu, 26 Dec 2019 12:32:27 -0500 Subject: [PATCH 339/686] [DOCS] Fix search request body link (#50498) --- docs/painless/painless-guide/painless-datetime.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/painless/painless-guide/painless-datetime.asciidoc b/docs/painless/painless-guide/painless-datetime.asciidoc index 17aed8fb90904..227119867b67c 100644 --- a/docs/painless/painless-guide/painless-datetime.asciidoc +++ b/docs/painless/painless-guide/painless-datetime.asciidoc @@ -825,7 +825,7 @@ GET /messages/_search?pretty=true ===== Age of a Message Script Field Example The following example uses a -{ref}/search-request-script-fields.html[script field] as part of the +{ref}/search-request-body.html#request-body-search-script-fields[script field] as part of the <> to display the elapsed time between "now" and when a message was received. From d2662eec17c078eca4c14cd9f98edeb20762c7bc Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Thu, 26 Dec 2019 14:20:51 -0500 Subject: [PATCH 340/686] [DOCS] Fix search request body links (#50500) PR #44238 changed several links related to the Elasticsearch search request body API. This updates several places still using outdated links or anchors. This will ultimately let us remove some redirects related to those link changes. --- docs/reference/query-dsl/has-parent-query.asciidoc | 2 +- docs/reference/search/request-body.asciidoc | 2 +- x-pack/docs/en/watcher/input/search.asciidoc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/query-dsl/has-parent-query.asciidoc b/docs/reference/query-dsl/has-parent-query.asciidoc index ba31069fad9f3..e2b8ee0f6446c 100644 --- a/docs/reference/query-dsl/has-parent-query.asciidoc +++ b/docs/reference/query-dsl/has-parent-query.asciidoc @@ -112,7 +112,7 @@ You can use this parameter to query multiple indices that may not contain the [[has-parent-query-performance]] ===== Sorting You cannot sort the results of a `has_parent` query using standard -<>. +<>. If you need to sort returned documents by a field in their parent documents, use a `function_score` query and sort by `_score`. For example, the following query diff --git a/docs/reference/search/request-body.asciidoc b/docs/reference/search/request-body.asciidoc index c3a9fe71e16b8..999ee05cb9dca 100644 --- a/docs/reference/search/request-body.asciidoc +++ b/docs/reference/search/request-body.asciidoc @@ -58,7 +58,7 @@ include::{docdir}/rest-api/common-parms.asciidoc[tag=index] `ccs_minimize_roundtrips`:: (Optional, boolean) If `true`, the network round-trips between the coordinating node and the remote clusters ewill be minimized when executing - {ccs} requests. See <> for more. Defaults to `true`. + {ccs} requests. See <>. Defaults to `true`. include::{docdir}/rest-api/common-parms.asciidoc[tag=from] diff --git a/x-pack/docs/en/watcher/input/search.asciidoc b/x-pack/docs/en/watcher/input/search.asciidoc index 9533a450395e6..7abef248f371a 100644 --- a/x-pack/docs/en/watcher/input/search.asciidoc +++ b/x-pack/docs/en/watcher/input/search.asciidoc @@ -163,7 +163,7 @@ accurately. |====== | Name |Required | Default | Description -| `request.search_type` | no | `query_then_fetch` | The <> +| `request.search_type` | no | `query_then_fetch` | The <> of search request to perform. Valid values are: `dfs_query_and_fetch`, `dfs_query_then_fetch`, `query_and_fetch`, and `query_then_fetch`. The Elasticsearch default is `query_then_fetch`. From 5ebd01a3a43736beac473113916642825279d81a Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Fri, 27 Dec 2019 11:00:51 -0500 Subject: [PATCH 341/686] [DOCS] Abbreviate token filter titles (#50511) --- .../analysis/tokenfilters/flatten-graph-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/hunspell-tokenfilter.asciidoc | 5 ++++- .../tokenfilters/keyword-marker-tokenfilter.asciidoc | 5 ++++- .../tokenfilters/keyword-repeat-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/kstem-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/minhash-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/multiplexer-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/normalization-tokenfilter.asciidoc | 5 ++++- .../tokenfilters/pattern-capture-tokenfilter.asciidoc | 5 ++++- .../tokenfilters/pattern_replace-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/phonetic-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/porterstem-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/predicate-tokenfilter.asciidoc | 5 ++++- .../tokenfilters/remove-duplicates-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/reverse-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/shingle-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/snowball-tokenfilter.asciidoc | 5 ++++- .../tokenfilters/stemmer-override-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/stemmer-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/stop-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/synonym-graph-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/synonym-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/trim-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/truncate-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/unique-tokenfilter.asciidoc | 5 ++++- .../analysis/tokenfilters/uppercase-tokenfilter.asciidoc | 5 ++++- .../tokenfilters/word-delimiter-graph-tokenfilter.asciidoc | 5 ++++- .../tokenfilters/word-delimiter-tokenfilter.asciidoc | 5 ++++- 28 files changed, 112 insertions(+), 28 deletions(-) diff --git a/docs/reference/analysis/tokenfilters/flatten-graph-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/flatten-graph-tokenfilter.asciidoc index 1495e8a91b2a7..bcff83c5e9950 100644 --- a/docs/reference/analysis/tokenfilters/flatten-graph-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/flatten-graph-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-flatten-graph-tokenfilter]] -=== Flatten Graph Token Filter +=== Flatten graph token filter +++++ +Flatten graph +++++ experimental[This functionality is marked as experimental in Lucene] diff --git a/docs/reference/analysis/tokenfilters/hunspell-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/hunspell-tokenfilter.asciidoc index 2f258b00ee96b..39f584bffe694 100644 --- a/docs/reference/analysis/tokenfilters/hunspell-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/hunspell-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-hunspell-tokenfilter]] -=== Hunspell Token Filter +=== Hunspell token filter +++++ +Hunspell +++++ Basic support for hunspell stemming. Hunspell dictionaries will be picked up from a dedicated hunspell directory on the filesystem diff --git a/docs/reference/analysis/tokenfilters/keyword-marker-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/keyword-marker-tokenfilter.asciidoc index ea9dcad8a6ca9..6ee9f41f777d1 100644 --- a/docs/reference/analysis/tokenfilters/keyword-marker-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/keyword-marker-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-keyword-marker-tokenfilter]] -=== Keyword Marker Token Filter +=== Keyword marker token filter +++++ +Keyword marker +++++ Protects words from being modified by stemmers. Must be placed before any stemming filters. diff --git a/docs/reference/analysis/tokenfilters/keyword-repeat-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/keyword-repeat-tokenfilter.asciidoc index ca15b2da5a8f5..58d86596bbf04 100644 --- a/docs/reference/analysis/tokenfilters/keyword-repeat-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/keyword-repeat-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-keyword-repeat-tokenfilter]] -=== Keyword Repeat Token Filter +=== Keyword repeat token filter +++++ +Keyword repeat +++++ The `keyword_repeat` token filter Emits each incoming token twice once as keyword and once as a non-keyword to allow an unstemmed version of a diff --git a/docs/reference/analysis/tokenfilters/kstem-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/kstem-tokenfilter.asciidoc index ff0695e64964f..3b7796bc596fa 100644 --- a/docs/reference/analysis/tokenfilters/kstem-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/kstem-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-kstem-tokenfilter]] -=== KStem Token Filter +=== KStem token filter +++++ +KStem +++++ The `kstem` token filter is a high performance filter for english. All terms must already be lowercased (use `lowercase` filter) for this diff --git a/docs/reference/analysis/tokenfilters/minhash-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/minhash-tokenfilter.asciidoc index 86e14c09d51cd..c6b134f8735be 100644 --- a/docs/reference/analysis/tokenfilters/minhash-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/minhash-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-minhash-tokenfilter]] -=== MinHash Token Filter +=== MinHash token filter +++++ +MinHash +++++ The `min_hash` token filter hashes each token of the token stream and divides the resulting hashes into buckets, keeping the lowest-valued hashes per diff --git a/docs/reference/analysis/tokenfilters/multiplexer-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/multiplexer-tokenfilter.asciidoc index c943c95defe2d..e12fa99324228 100644 --- a/docs/reference/analysis/tokenfilters/multiplexer-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/multiplexer-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-multiplexer-tokenfilter]] -=== Multiplexer Token Filter +=== Multiplexer token filter +++++ +Multiplexer +++++ A token filter of type `multiplexer` will emit multiple tokens at the same position, each version of the token having been run through a different filter. Identical diff --git a/docs/reference/analysis/tokenfilters/normalization-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/normalization-tokenfilter.asciidoc index 2ff8ab134972a..85f33d3f38490 100644 --- a/docs/reference/analysis/tokenfilters/normalization-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/normalization-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-normalization-tokenfilter]] -=== Normalization Token Filter +=== Normalization token filters +++++ +Normalization +++++ There are several token filters available which try to normalize special characters of a certain language. diff --git a/docs/reference/analysis/tokenfilters/pattern-capture-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/pattern-capture-tokenfilter.asciidoc index 0b5aa62029fea..7b9a3b3199040 100644 --- a/docs/reference/analysis/tokenfilters/pattern-capture-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/pattern-capture-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-pattern-capture-tokenfilter]] -=== Pattern Capture Token Filter +=== Pattern capture token filter +++++ +Pattern capture +++++ The `pattern_capture` token filter, unlike the `pattern` tokenizer, emits a token for every capture group in the regular expression. diff --git a/docs/reference/analysis/tokenfilters/pattern_replace-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/pattern_replace-tokenfilter.asciidoc index bc8cdc385bf56..85ddd236556f0 100644 --- a/docs/reference/analysis/tokenfilters/pattern_replace-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/pattern_replace-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-pattern_replace-tokenfilter]] -=== Pattern Replace Token Filter +=== Pattern replace token filter +++++ +Pattern replace +++++ The `pattern_replace` token filter allows to easily handle string replacements based on a regular expression. The regular expression is diff --git a/docs/reference/analysis/tokenfilters/phonetic-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/phonetic-tokenfilter.asciidoc index 4a7324acc39a7..cceac39e691ca 100644 --- a/docs/reference/analysis/tokenfilters/phonetic-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/phonetic-tokenfilter.asciidoc @@ -1,4 +1,7 @@ [[analysis-phonetic-tokenfilter]] -=== Phonetic Token Filter +=== Phonetic token filter +++++ +Phonetic +++++ The `phonetic` token filter is provided as the {plugins}/analysis-phonetic.html[`analysis-phonetic`] plugin. diff --git a/docs/reference/analysis/tokenfilters/porterstem-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/porterstem-tokenfilter.asciidoc index fc2edf526c372..519618c2b2108 100644 --- a/docs/reference/analysis/tokenfilters/porterstem-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/porterstem-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-porterstem-tokenfilter]] -=== Porter Stem Token Filter +=== Porter stem token filter +++++ +Porter stem +++++ A token filter of type `porter_stem` that transforms the token stream as per the Porter stemming algorithm. diff --git a/docs/reference/analysis/tokenfilters/predicate-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/predicate-tokenfilter.asciidoc index e21e4e5690f60..2360b386aae55 100644 --- a/docs/reference/analysis/tokenfilters/predicate-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/predicate-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-predicatefilter-tokenfilter]] -=== Predicate Token Filter Script +=== Predicate script token filter +++++ +Predicate script +++++ The predicate_token_filter token filter takes a predicate script, and removes tokens that do not match the predicate. diff --git a/docs/reference/analysis/tokenfilters/remove-duplicates-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/remove-duplicates-tokenfilter.asciidoc index 594e18eaf7f7e..e9dbf1ed15303 100644 --- a/docs/reference/analysis/tokenfilters/remove-duplicates-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/remove-duplicates-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-remove-duplicates-tokenfilter]] -=== Remove Duplicates Token Filter +=== Remove duplicates token filter +++++ +Remove duplicates +++++ A token filter of type `remove_duplicates` that drops identical tokens at the same position. diff --git a/docs/reference/analysis/tokenfilters/reverse-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/reverse-tokenfilter.asciidoc index b00499815553b..08eaa796951c8 100644 --- a/docs/reference/analysis/tokenfilters/reverse-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/reverse-tokenfilter.asciidoc @@ -1,4 +1,7 @@ [[analysis-reverse-tokenfilter]] -=== Reverse Token Filter +=== Reverse token filter +++++ +Reverse +++++ A token filter of type `reverse` that simply reverses each token. diff --git a/docs/reference/analysis/tokenfilters/shingle-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/shingle-tokenfilter.asciidoc index a6d544fc7b39d..5f6bec96dbae7 100644 --- a/docs/reference/analysis/tokenfilters/shingle-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/shingle-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-shingle-tokenfilter]] -=== Shingle Token Filter +=== Shingle token filter +++++ +Shingle +++++ NOTE: Shingles are generally used to help speed up phrase queries. Rather than building filter chains by hand, you may find it easier to use the diff --git a/docs/reference/analysis/tokenfilters/snowball-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/snowball-tokenfilter.asciidoc index bafb4fb7f7734..df1df3a43cb31 100644 --- a/docs/reference/analysis/tokenfilters/snowball-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/snowball-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-snowball-tokenfilter]] -=== Snowball Token Filter +=== Snowball token filter +++++ +Snowball +++++ A filter that stems words using a Snowball-generated stemmer. The `language` parameter controls the stemmer with the following available diff --git a/docs/reference/analysis/tokenfilters/stemmer-override-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/stemmer-override-tokenfilter.asciidoc index d2fbba841808e..94d64bb82ea9b 100644 --- a/docs/reference/analysis/tokenfilters/stemmer-override-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/stemmer-override-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-stemmer-override-tokenfilter]] -=== Stemmer Override Token Filter +=== Stemmer override token filter +++++ +Stemmer override +++++ Overrides stemming algorithms, by applying a custom mapping, then protecting these terms from being modified by stemmers. Must be placed diff --git a/docs/reference/analysis/tokenfilters/stemmer-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/stemmer-tokenfilter.asciidoc index 29ae8e96606c7..4e98e24d08ef0 100644 --- a/docs/reference/analysis/tokenfilters/stemmer-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/stemmer-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-stemmer-tokenfilter]] -=== Stemmer Token Filter +=== Stemmer token filter +++++ +Stemmer +++++ // Adds attribute for the 'minimal_portuguese' stemmer values link. // This link contains ~, which is converted to subscript. diff --git a/docs/reference/analysis/tokenfilters/stop-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/stop-tokenfilter.asciidoc index f4019fa1800e9..3263d89e2b3ff 100644 --- a/docs/reference/analysis/tokenfilters/stop-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/stop-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-stop-tokenfilter]] -=== Stop Token Filter +=== Stop token filter +++++ +Stop +++++ A token filter of type `stop` that removes stop words from token streams. diff --git a/docs/reference/analysis/tokenfilters/synonym-graph-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/synonym-graph-tokenfilter.asciidoc index 63e037de486f8..13e7609ac7ee2 100644 --- a/docs/reference/analysis/tokenfilters/synonym-graph-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/synonym-graph-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-synonym-graph-tokenfilter]] -=== Synonym Graph Token Filter +=== Synonym graph token filter +++++ +Synonym graph +++++ The `synonym_graph` token filter allows to easily handle synonyms, including multi-word synonyms correctly during the analysis process. diff --git a/docs/reference/analysis/tokenfilters/synonym-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/synonym-tokenfilter.asciidoc index 3c0e967afc4bd..58caf8bb28281 100644 --- a/docs/reference/analysis/tokenfilters/synonym-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/synonym-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-synonym-tokenfilter]] -=== Synonym Token Filter +=== Synonym token filter +++++ +Synonym +++++ The `synonym` token filter allows to easily handle synonyms during the analysis process. Synonyms are configured using a configuration file. diff --git a/docs/reference/analysis/tokenfilters/trim-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/trim-tokenfilter.asciidoc index 34a0e93a3af22..1373811b0cb82 100644 --- a/docs/reference/analysis/tokenfilters/trim-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/trim-tokenfilter.asciidoc @@ -1,4 +1,7 @@ [[analysis-trim-tokenfilter]] -=== Trim Token Filter +=== Trim token filter +++++ +Trim +++++ The `trim` token filter trims the whitespace surrounding a token. diff --git a/docs/reference/analysis/tokenfilters/truncate-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/truncate-tokenfilter.asciidoc index 4c28ddba38146..c1d171dbbbbc0 100644 --- a/docs/reference/analysis/tokenfilters/truncate-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/truncate-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-truncate-tokenfilter]] -=== Truncate Token Filter +=== Truncate token filter +++++ +Truncate +++++ The `truncate` token filter can be used to truncate tokens into a specific length. diff --git a/docs/reference/analysis/tokenfilters/unique-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/unique-tokenfilter.asciidoc index 8b42f6b73b934..6ce084c183ff4 100644 --- a/docs/reference/analysis/tokenfilters/unique-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/unique-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-unique-tokenfilter]] -=== Unique Token Filter +=== Unique token filter +++++ +Unique +++++ The `unique` token filter can be used to only index unique tokens during analysis. By default it is applied on all the token stream. If diff --git a/docs/reference/analysis/tokenfilters/uppercase-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/uppercase-tokenfilter.asciidoc index 639d1e9106849..c745f247ec3d9 100644 --- a/docs/reference/analysis/tokenfilters/uppercase-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/uppercase-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-uppercase-tokenfilter]] -=== Uppercase Token Filter +=== Uppercase token filter +++++ +Uppercase +++++ A token filter of type `uppercase` that normalizes token text to upper case. diff --git a/docs/reference/analysis/tokenfilters/word-delimiter-graph-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/word-delimiter-graph-tokenfilter.asciidoc index 4acd0163109a4..66e7b18c74426 100644 --- a/docs/reference/analysis/tokenfilters/word-delimiter-graph-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/word-delimiter-graph-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-word-delimiter-graph-tokenfilter]] -=== Word Delimiter Graph Token Filter +=== Word delimiter graph token filter +++++ +Word delimiter graph +++++ experimental[This functionality is marked as experimental in Lucene] diff --git a/docs/reference/analysis/tokenfilters/word-delimiter-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/word-delimiter-tokenfilter.asciidoc index 1c07176430eed..d0cea87176d41 100644 --- a/docs/reference/analysis/tokenfilters/word-delimiter-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/word-delimiter-tokenfilter.asciidoc @@ -1,5 +1,8 @@ [[analysis-word-delimiter-tokenfilter]] -=== Word Delimiter Token Filter +=== Word delimiter token filter +++++ +Word delimiter +++++ Named `word_delimiter`, it Splits words into subwords and performs optional transformations on subword groups. Words are split into From 30b4133872dcfb0abbf79b291cc735f11b4d90ba Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 27 Dec 2019 13:41:05 -0500 Subject: [PATCH 342/686] Adjust tests after always enable soft-deletes in 8.0 The InternalEngine always enables soft deletes in 8.0 regardless of the setting. We need to wait for the global checkpoint and peer recovery retention leases to be synced in these tests. Relates #50415 --- .../index/shard/IndexShardTests.java | 14 ++++++------- .../indices/stats/IndexStatsIT.java | 20 +++++++++---------- .../SharedClusterSnapshotRestoreIT.java | 9 ++------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 21d3c95c5942f..6a8a63ce17f32 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -3511,14 +3511,12 @@ public void testSegmentMemoryTrackedInBreaker() throws Exception { // Here we are testing that a fully deleted segment should be dropped and its memory usage is freed. // In order to instruct the merge policy not to keep a fully deleted segment, // we need to flush and make that commit safe so that the SoftDeletesPolicy can drop everything. - if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { - primary.updateGlobalCheckpointForShard( - primary.routingEntry().allocationId().getId(), - primary.getLastSyncedGlobalCheckpoint()); - primary.syncRetentionLeases(); - primary.sync(); - flushShard(primary); - } + primary.updateGlobalCheckpointForShard( + primary.routingEntry().allocationId().getId(), + primary.getLastSyncedGlobalCheckpoint()); + primary.syncRetentionLeases(); + primary.sync(); + flushShard(primary); primary.refresh("force refresh"); ss = primary.segmentStats(randomBoolean(), randomBoolean()); diff --git a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java index a77091a2ce93e..70eb62e127c75 100644 --- a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java +++ b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java @@ -1017,17 +1017,15 @@ public void testFilterCacheStats() throws Exception { // Here we are testing that a fully deleted segment should be dropped and its cached is evicted. // In order to instruct the merge policy not to keep a fully deleted segment, // we need to flush and make that commit safe so that the SoftDeletesPolicy can drop everything. - if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { - persistGlobalCheckpoint("index"); - assertBusy(() -> { - for (final ShardStats shardStats : client().admin().indices().prepareStats("index").get().getIndex("index").getShards()) { - final long maxSeqNo = shardStats.getSeqNoStats().getMaxSeqNo(); - assertTrue(shardStats.getRetentionLeaseStats().retentionLeases().leases().stream() - .allMatch(retentionLease -> retentionLease.retainingSequenceNumber() == maxSeqNo + 1)); - } - }); - flush("index"); - } + persistGlobalCheckpoint("index"); + assertBusy(() -> { + for (final ShardStats shardStats : client().admin().indices().prepareStats("index").get().getIndex("index").getShards()) { + final long maxSeqNo = shardStats.getSeqNoStats().getMaxSeqNo(); + assertTrue(shardStats.getRetentionLeaseStats().retentionLeases().leases().stream() + .allMatch(retentionLease -> retentionLease.retainingSequenceNumber() == maxSeqNo + 1)); + } + }); + flush("index"); logger.info("--> force merging to a single segment"); ForceMergeResponse forceMergeResponse = client().admin().indices().prepareForceMerge("index").setFlush(true).setMaxNumSegments(1).get(); diff --git a/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java b/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java index e31ad37296093..b54359e516d00 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java @@ -117,7 +117,6 @@ import static org.elasticsearch.cluster.routing.allocation.decider.MaxRetryAllocationDecider.SETTING_ALLOCATION_MAX_RETRY; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.IndexSettings.INDEX_REFRESH_INTERVAL_SETTING; -import static org.elasticsearch.index.IndexSettings.INDEX_SOFT_DELETES_SETTING; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; import static org.elasticsearch.index.shard.IndexShardTests.getEngineFromShard; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -2325,12 +2324,8 @@ public void testSnapshotMoreThanOnce() throws ExecutionException, InterruptedExc List shards = snapshotStatus.getShards(); for (SnapshotIndexShardStatus status : shards) { // we flush before the snapshot such that we have to process the segments_N files plus the .del file - if (INDEX_SOFT_DELETES_SETTING.get(settings)) { - // soft-delete generates DV files. - assertThat(status.getStats().getProcessedFileCount(), greaterThan(2)); - } else { - assertThat(status.getStats().getProcessedFileCount(), equalTo(2)); - } + // soft-delete generates DV files. + assertThat(status.getStats().getProcessedFileCount(), greaterThan(2)); } } } From e163058801b0a599a5356315f4f514ccde37b5a7 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Sun, 29 Dec 2019 16:40:42 -0500 Subject: [PATCH 343/686] Fix hard-deletes engine simulation (#50517) The test "testRecoverFromHardDeletesIndex" failed because the "min_retained_seqno" commit tag exists after we index using a hard-deletes engine. A hard-deletes engine must not create this commit tag; hence we need to remove it in the test. Relates #50415 --- .../index/engine/InternalEngineTests.java | 15 +++++++++++++++ .../index/engine/EngineTestCase.java | 9 +++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 9a2f79b7530a7..b9fe4c7dc4d98 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -5464,7 +5464,22 @@ public long softUpdateDocuments(Term term, Iterable userData = new HashMap<>(store.readLastCommittedSegmentsInfo().userData); + userData.remove(Engine.MIN_RETAINED_SEQNO); + IndexWriterConfig indexWriterConfig = new IndexWriterConfig(null) + .setOpenMode(IndexWriterConfig.OpenMode.APPEND) + .setIndexCreatedVersionMajor(Version.CURRENT.luceneVersion.major) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) + .setCommitOnClose(false) + .setMergePolicy(NoMergePolicy.INSTANCE); + try (IndexWriter writer = new IndexWriter(store.directory(), indexWriterConfig)) { + writer.setLiveCommitData(userData.entrySet()); + writer.commit(); + } try (InternalEngine softDeletesEngine = new InternalEngine(config)) { // do not recover from translog + assertThat(softDeletesEngine.getLastCommittedSegmentInfos().userData, equalTo(userData)); assertThat(softDeletesEngine.getVersionMap().keySet(), empty()); softDeletesEngine.recoverFromTranslog(translogHandler, Long.MAX_VALUE); if (randomBoolean()) { diff --git a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java index 81b2bdd8385ec..6ec344783c8f2 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java @@ -1088,15 +1088,16 @@ public static void assertConsistentHistoryBetweenTranslogAndLuceneIndex(Engine e } final long globalCheckpoint = EngineTestCase.getTranslog(engine).getLastSyncedGlobalCheckpoint(); final long retainedOps = engine.config().getIndexSettings().getSoftDeleteRetentionOperations(); - final long seqNoForRecovery; + final long minSeqNoToRetain; if (engine.config().getIndexSettings().isSoftDeleteEnabled()) { try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { - seqNoForRecovery = Long.parseLong(safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; + final long seqNoForRecovery = Long.parseLong( + safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; + minSeqNoToRetain = Math.min(seqNoForRecovery, globalCheckpoint + 1 - retainedOps); } } else { - seqNoForRecovery = engine.getMinRetainedSeqNo(); + minSeqNoToRetain = engine.getMinRetainedSeqNo(); } - final long minSeqNoToRetain = Math.min(seqNoForRecovery, globalCheckpoint + 1 - retainedOps); for (Translog.Operation translogOp : translogOps) { final Translog.Operation luceneOp = luceneOps.get(translogOp.seqNo()); if (luceneOp == null) { From 85af0dd5820fdc57cfd22148d3346e071fe52636 Mon Sep 17 00:00:00 2001 From: riverbuilding Date: Mon, 30 Dec 2019 08:48:10 -0500 Subject: [PATCH 344/686] [DOCS] Correct Painless operator typos (#50472) --- .../painless-lang-spec/painless-operators-numeric.asciidoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/painless/painless-lang-spec/painless-operators-numeric.asciidoc b/docs/painless/painless-lang-spec/painless-operators-numeric.asciidoc index 22c2f04d50677..80fedd6e9b69c 100644 --- a/docs/painless/painless-lang-spec/painless-operators-numeric.asciidoc +++ b/docs/painless/painless-lang-spec/painless-operators-numeric.asciidoc @@ -1204,7 +1204,7 @@ The following table illustrates the resultant bit from the xoring of two bits. [source,ANTLR4] ---- -bitwise_and: expression '^' expression; +bitwise_xor: expression '^' expression; ---- *Promotion* @@ -1284,7 +1284,7 @@ The following table illustrates the resultant bit from the oring of two bits. [source,ANTLR4] ---- -bitwise_and: expression '|' expression; +bitwise_or: expression '|' expression; ---- *Promotion* @@ -1336,4 +1336,4 @@ def y = x ^ 8; <2> implicit cast `def` to `int 7`; bitwise or `int 7` and `int 8` -> `int 15`; implicit cast `int 15` to `def` -> `def`; - store `def` to `y` \ No newline at end of file + store `def` to `y` From 8d3deeb5e181c630deee4040b9cf54c9e77efd11 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 30 Dec 2019 07:03:38 -0800 Subject: [PATCH 345/686] [DOCS] Adds intro for OIDC realm (#50485) --- .../en/security/authentication/index.asciidoc | 1 + .../authentication/oidc-realm.asciidoc | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 x-pack/docs/en/security/authentication/oidc-realm.asciidoc diff --git a/x-pack/docs/en/security/authentication/index.asciidoc b/x-pack/docs/en/security/authentication/index.asciidoc index 3055a6024ab10..7b20e3220ea40 100644 --- a/x-pack/docs/en/security/authentication/index.asciidoc +++ b/x-pack/docs/en/security/authentication/index.asciidoc @@ -9,6 +9,7 @@ include::active-directory-realm.asciidoc[] include::file-realm.asciidoc[] include::ldap-realm.asciidoc[] include::native-realm.asciidoc[] +include::oidc-realm.asciidoc[] include::pki-realm.asciidoc[] include::saml-realm.asciidoc[] include::kerberos-realm.asciidoc[] diff --git a/x-pack/docs/en/security/authentication/oidc-realm.asciidoc b/x-pack/docs/en/security/authentication/oidc-realm.asciidoc new file mode 100644 index 0000000000000..7d47e5288eb10 --- /dev/null +++ b/x-pack/docs/en/security/authentication/oidc-realm.asciidoc @@ -0,0 +1,19 @@ +[role="xpack"] +[[oidc-realm]] +=== OpenID Connect authentication + +The OpenID Connect realm enables {es} to serve as an OpenID Connect Relying +Party (RP) and provides single sign-on (SSO) support in {kib}. + +It is specifically designed to support authentication via an interactive web +browser, so it does not operate as a standard authentication realm. Instead, +there are {kib} and {es} {security-features} that work together to enable +interactive OpenID Connect sessions. + +This means that the OpenID Connect realm is not suitable for use by standard +REST clients. If you configure an OpenID Connect realm for use in {kib}, you +should also configure another realm, such as the <> +in your authentication chain. + +In order to simplify the process of configuring OpenID Connect authentication +within the {stack}, there is a step-by-step guide: <>. From c29cdc1352b5d35c8c7dd960a5bf015eb0175851 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Mon, 30 Dec 2019 11:30:30 -0500 Subject: [PATCH 346/686] Replace synced-flush with flush in rolling upgrade to 8.0 (#50524) This change recommends using a regular flush instead of synced-flush in a rolling upgrade from 7.x to 8.0. We can perform noop recoveries with a regular flush --- docs/reference/upgrade/rolling_upgrade.asciidoc | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/reference/upgrade/rolling_upgrade.asciidoc b/docs/reference/upgrade/rolling_upgrade.asciidoc index 96b3d12fb0264..02a85814537a5 100644 --- a/docs/reference/upgrade/rolling_upgrade.asciidoc +++ b/docs/reference/upgrade/rolling_upgrade.asciidoc @@ -36,15 +36,17 @@ To perform a rolling upgrade to {version}: include::disable-shard-alloc.asciidoc[] -- -. *Stop non-essential indexing and perform a synced flush.* (Optional) +. *Stop non-essential indexing and perform a flush.* (Optional) + -- While you can continue indexing during the upgrade, shard recovery is much faster if you temporarily stop non-essential indexing and perform a -<>. - -include::synced-flush.asciidoc[] +<>. +[source,console] +-------------------------------------------------- +POST /_flush +-------------------------------------------------- -- . *Temporarily stop the tasks associated with active {ml} jobs and {dfeeds}.* (Optional) @@ -148,7 +150,7 @@ As soon as another node is upgraded, the replicas can be assigned and the status will change to `green`. ==================================================== -Shards that were not <> might take longer to +Shards that were not <> might take longer to recover. You can monitor the recovery status of individual shards by submitting a <> request: From e24506680487fdfe1142f91d75a8bb1f331243ff Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 30 Dec 2019 15:38:59 -0500 Subject: [PATCH 347/686] [DOCS] Bump copyright to 2019 for Java HLRC license (#50206) --- docs/java-rest/license.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java-rest/license.asciidoc b/docs/java-rest/license.asciidoc index 687974868274e..2ec3d15197439 100644 --- a/docs/java-rest/license.asciidoc +++ b/docs/java-rest/license.asciidoc @@ -1,6 +1,6 @@ == License -Copyright 2013-2018 Elasticsearch +Copyright 2013-2019 Elasticsearch Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From dc3c829375bcf32f6d38da1d215717b7027863ab Mon Sep 17 00:00:00 2001 From: lcawl Date: Mon, 30 Dec 2019 15:21:18 -0800 Subject: [PATCH 348/686] [DOCS] Minor fixes in ML APIs --- .../anomaly-detection/apis/close-job.asciidoc | 4 +- .../apis/delete-calendar-event.asciidoc | 3 -- .../apis/delete-calendar-job.asciidoc | 3 -- .../apis/delete-calendar.asciidoc | 2 - .../apis/delete-datafeed.asciidoc | 2 - .../apis/delete-filter.asciidoc | 2 - .../apis/delete-forecast.asciidoc | 2 - .../apis/delete-job.asciidoc | 2 - .../apis/delete-snapshot.asciidoc | 2 - .../anomaly-detection/apis/flush-job.asciidoc | 6 +-- .../anomaly-detection/apis/forecast.asciidoc | 5 +- .../apis/get-bucket.asciidoc | 51 +++++++++---------- .../apis/get-calendar-event.asciidoc | 3 -- .../apis/get-calendar.asciidoc | 3 -- .../apis/get-datafeed.asciidoc | 3 -- .../apis/get-filter.asciidoc | 3 -- .../apis/get-job-stats.asciidoc | 2 +- .../anomaly-detection/apis/get-job.asciidoc | 2 - .../apis/get-overall-buckets.asciidoc | 3 -- .../anomaly-detection/apis/open-job.asciidoc | 7 +-- .../apis/post-calendar-event.asciidoc | 2 - .../apis/put-datafeed.asciidoc | 2 - .../apis/put-filter.asciidoc | 2 - .../anomaly-detection/apis/put-job.asciidoc | 2 - .../apis/start-datafeed.asciidoc | 8 ++- .../apis/stop-datafeed.asciidoc | 6 +-- .../apis/update-datafeed.asciidoc | 4 -- .../apis/update-filter.asciidoc | 3 -- .../apis/update-job.asciidoc | 2 - .../apis/update-snapshot.asciidoc | 2 - .../apis/validate-detector.asciidoc | 2 - .../apis/validate-job.asciidoc | 2 - 32 files changed, 37 insertions(+), 110 deletions(-) diff --git a/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc index 7f9fd0489ab77..0700d92ba7283 100644 --- a/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc @@ -92,9 +92,9 @@ The following example closes the `total-requests` job: [source,console] -------------------------------------------------- -POST _ml/anomaly_detectors/total-requests/_close +POST _ml/anomaly_detectors/low_request_rate/_close -------------------------------------------------- -// TEST[skip:sometimes fails due to https://github.com/elastic/elasticsearch/pull/48583#issuecomment-552991325 - on unmuting use setup:server_metrics_openjob-raw] +// TEST[skip:sometimes fails due to https://github.com/elastic/elasticsearch/pull/48583#issuecomment-552991325] When the job is closed, you receive the following results: diff --git a/docs/reference/ml/anomaly-detection/apis/delete-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-calendar-event.asciidoc index e0be1e135afcf..d401d7dc052d4 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-calendar-event.asciidoc @@ -40,9 +40,6 @@ events and delete the calendar, see the [[ml-delete-calendar-event-example]] ==== {api-examples-title} -The following example deletes a scheduled event from the `planned-outages` -calendar: - [source,console] -------------------------------------------------- DELETE _ml/calendars/planned-outages/events/LS8LJGEBMTCMA-qz49st diff --git a/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc index 6b705b8c6f932..12f7b88769dd8 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc @@ -33,9 +33,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection-list] [[ml-delete-calendar-job-example]] ==== {api-examples-title} -The following example removes the association between the `planned-outages` -calendar and `total-requests` job: - [source,console] -------------------------------------------------- DELETE _ml/calendars/planned-outages/jobs/total-requests diff --git a/docs/reference/ml/anomaly-detection/apis/delete-calendar.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-calendar.asciidoc index c9dad6bbba19c..e9ac45e35600a 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-calendar.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-calendar.asciidoc @@ -35,8 +35,6 @@ calendar. [[ml-delete-calendar-example]] ==== {api-examples-title} -The following example deletes the `planned-outages` calendar: - [source,console] -------------------------------------------------- DELETE _ml/calendars/planned-outages diff --git a/docs/reference/ml/anomaly-detection/apis/delete-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-datafeed.asciidoc index d933afe4f9a42..a555dd9285c5b 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-datafeed.asciidoc @@ -41,8 +41,6 @@ quicker than stopping and deleting the {dfeed}. [[ml-delete-datafeed-example]] ==== {api-examples-title} -The following example deletes the `datafeed-total-requests` {dfeed}: - [source,console] -------------------------------------------------- DELETE _ml/datafeeds/datafeed-total-requests diff --git a/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc index ffb2f7b894cc4..2894aaefb7a89 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc @@ -36,8 +36,6 @@ update or delete the job before you can delete the filter. [[ml-delete-filter-example]] ==== {api-examples-title} -The following example deletes the `safe_domains` filter: - [source,console] -------------------------------------------------- DELETE _ml/filters/safe_domains diff --git a/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc index 80eb03c75b59d..e3b0161a17f64 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-forecast.asciidoc @@ -67,8 +67,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-delete-forecast-example]] ==== {api-examples-title} -The following example deletes all forecasts from the `total-requests` job: - [source,console] -------------------------------------------------- DELETE _ml/anomaly_detectors/total-requests/_forecast/_all diff --git a/docs/reference/ml/anomaly-detection/apis/delete-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-job.asciidoc index 0c04ec468bbbf..cb2d59fbc825a 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-job.asciidoc @@ -56,8 +56,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-delete-job-example]] ==== {api-examples-title} -The following example deletes the `total-requests` job: - [source,console] -------------------------------------------------- DELETE _ml/anomaly_detectors/total-requests diff --git a/docs/reference/ml/anomaly-detection/apis/delete-snapshot.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-snapshot.asciidoc index 90d0303b9aff3..0894d6373d874 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-snapshot.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-snapshot.asciidoc @@ -40,8 +40,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=snapshot-id] [[ml-delete-snapshot-example]] ==== {api-examples-title} -The following example deletes the `1491948163` snapshot: - [source,console] -------------------------------------------------- DELETE _ml/anomaly_detectors/farequote/model_snapshots/1491948163 diff --git a/docs/reference/ml/anomaly-detection/apis/flush-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/flush-job.asciidoc index f6e81a3b26131..510e5b9a7b863 100644 --- a/docs/reference/ml/anomaly-detection/apis/flush-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/flush-job.asciidoc @@ -67,16 +67,14 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-flush-job-example]] ==== {api-examples-title} -The following example flushes the `total-requests` job: - [source,console] -------------------------------------------------- -POST _ml/anomaly_detectors/total-requests/_flush +POST _ml/anomaly_detectors/low_request_rate/_flush { "calc_interim": true } -------------------------------------------------- -// TEST[skip:setup:server_metrics_openjob] +// TEST[skip:Kibana sample data] When the operation succeeds, you receive the following results: diff --git a/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc b/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc index 61b8981843184..f083639ff248b 100644 --- a/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/forecast.asciidoc @@ -57,11 +57,9 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-forecast-example]] ==== {api-examples-title} -The following example requests a 10 day forecast for the `total-requests` job: - [source,console] -------------------------------------------------- -POST _ml/anomaly_detectors/total-requests/_forecast +POST _ml/anomaly_detectors/low_request_rate/_forecast { "duration": "10d" } @@ -79,4 +77,3 @@ When the forecast is created, you receive the following results: // NOTCONSOLE You can subsequently see the forecast in the *Single Metric Viewer* in {kib}. - diff --git a/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc index a34d6bc714a99..a3e5606f25d82 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-bucket.asciidoc @@ -170,50 +170,47 @@ the results for the bucket. [[ml-get-bucket-example]] ==== {api-examples-title} -The following example gets bucket information for the `it-ops-kpi` job: - [source,console] -------------------------------------------------- -GET _ml/anomaly_detectors/it-ops-kpi/results/buckets +GET _ml/anomaly_detectors/low_request_rate/results/buckets { "anomaly_score": 80, "start": "1454530200001" } -------------------------------------------------- -// TEST[skip:todo] +// TEST[skip:Kibana sample data] In this example, the API returns a single result that matches the specified score and time constraints: [source,js] ---- { - "count": 1, - "buckets": [ + "count" : 1, + "buckets" : [ { - "job_id": "it-ops-kpi", - "timestamp": 1454943900000, - "anomaly_score": 94.1706, - "bucket_span": 300, - "initial_anomaly_score": 94.1706, - "event_count": 153, - "is_interim": false, - "bucket_influencers": [ + "job_id" : "low_request_rate", + "timestamp" : 1578398400000, + "anomaly_score" : 91.58505459594764, + "bucket_span" : 3600, + "initial_anomaly_score" : 91.58505459594764, + "event_count" : 0, + "is_interim" : false, + "bucket_influencers" : [ { - "job_id": "it-ops-kpi", - "result_type": "bucket_influencer", - "influencer_field_name": "bucket_time", - "initial_anomaly_score": 94.1706, - "anomaly_score": 94.1706, - "raw_anomaly_score": 2.32119, - "probability": 0.00000575042, - "timestamp": 1454943900000, - "bucket_span": 300, - "is_interim": false + "job_id" : "low_request_rate", + "result_type" : "bucket_influencer", + "influencer_field_name" : "bucket_time", + "initial_anomaly_score" : 91.58505459594764, + "anomaly_score" : 91.58505459594764, + "raw_anomaly_score" : 0.5758246639716365, + "probability" : 1.7340849573442696E-4, + "timestamp" : 1578398400000, + "bucket_span" : 3600, + "is_interim" : false } ], - "processing_time_ms": 2, - "partition_scores": [], - "result_type": "bucket" + "processing_time_ms" : 0, + "result_type" : "bucket" } ] } diff --git a/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc index f3fcd65aab210..22fdc718c5d8e 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc @@ -80,9 +80,6 @@ following properties: [[ml-get-calendar-event-example]] ==== {api-examples-title} -The following example gets information about the scheduled events in the -`planned-outages` calendar: - [source,console] -------------------------------------------------- GET _ml/calendars/planned-outages/events diff --git a/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc index 0d0d85090df69..9c56a7742de5b 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc @@ -62,9 +62,6 @@ properties: [[ml-get-calendar-example]] ==== {api-examples-title} -The following example gets configuration information for the `planned-outages` -calendar: - [source,console] -------------------------------------------------- GET _ml/calendars/planned-outages diff --git a/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc index 368824aea6d51..8e96235b7fb05 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-datafeed.asciidoc @@ -73,9 +73,6 @@ see <>. [[ml-get-datafeed-example]] ==== {api-examples-title} -The following example gets configuration information for the -`datafeed-high_sum_total_sales` {dfeed}: - [source,console] -------------------------------------------------- GET _ml/datafeeds/datafeed-high_sum_total_sales diff --git a/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc index d53155681c7fa..9da2f44c198c7 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc @@ -61,9 +61,6 @@ properties: [[ml-get-filter-example]] ==== {api-examples-title} -The following example gets configuration information for the `safe_domains` -filter: - [source,console] -------------------------------------------------- GET _ml/filters/safe_domains diff --git a/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc index 9b9038abaa95b..9bf583595494d 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-job-stats.asciidoc @@ -165,7 +165,7 @@ NOTE: Unless there is at least one forecast, `memory_bytes`, `records`, `forecasts_stats`.`records`::: (object) Statistics about the number of forecast records: minimum, maximum, -saverage and total. +average and total. `forecasts_stats`.`processing_time_ms`::: (object) Statistics about the forecast runtime in milliseconds: minimum, maximum, diff --git a/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc index 5f4da902734fe..0eab920cff7ea 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-job.asciidoc @@ -86,8 +86,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=model-snapshot-id] [[ml-get-job-example]] ==== {api-examples-title} -//The following example gets configuration information for the `total-requests` job: - [source,console] -------------------------------------------------- GET _ml/anomaly_detectors/high_sum_total_sales diff --git a/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc index ab1b5588cccfd..6b0c6bf503918 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-overall-buckets.asciidoc @@ -120,9 +120,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=timestamp-results] [[ml-get-overall-buckets-example]] ==== {api-examples-title} -The following example gets overall buckets for {anomaly-jobs} with IDs matching -`job-*`: - [source,console] -------------------------------------------------- GET _ml/anomaly_detectors/job-*/results/overall_buckets diff --git a/docs/reference/ml/anomaly-detection/apis/open-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/open-job.asciidoc index 3651834480f10..39f3488201925 100644 --- a/docs/reference/ml/anomaly-detection/apis/open-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/open-job.asciidoc @@ -50,17 +50,14 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] [[ml-open-job-example]] ==== {api-examples-title} -The following example opens the `total-requests` job and sets an optional -property: - [source,console] -------------------------------------------------- -POST _ml/anomaly_detectors/total-requests/_open +POST _ml/anomaly_detectors/low_request_rate/_open { "timeout": "35m" } -------------------------------------------------- -// TEST[skip:setup:server_metrics_job] +// TEST[skip:Kibana sample data] When the job opens, you receive the following results: diff --git a/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc index 8a621f1faafff..76e7111fcd0cb 100644 --- a/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc @@ -58,8 +58,6 @@ of which must have a start time, end time, and description. [[ml-post-calendar-event-example]] ==== {api-examples-title} -You can add scheduled events to the `planned-outages` calendar as follows: - [source,console] -------------------------------------------------- POST _ml/calendars/planned-outages/events diff --git a/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc index a5a168b9f9c14..796d4995404a9 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-datafeed.asciidoc @@ -96,8 +96,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=scroll-size] [[ml-put-datafeed-example]] ==== {api-examples-title} -The following example creates the `datafeed-total-requests` {dfeed}: - [source,console] -------------------------------------------------- PUT _ml/datafeeds/datafeed-total-requests diff --git a/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc index 86245d84dbb28..3168389690a75 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc @@ -47,8 +47,6 @@ the `custom_rules` property of detector configuration objects. [[ml-put-filter-example]] ==== {api-examples-title} -The following example creates the `safe_domains` filter: - [source,console] -------------------------------------------------- PUT _ml/filters/safe_domains diff --git a/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc index 16c14c5faae41..1c4e59e1341be 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-job.asciidoc @@ -231,8 +231,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=results-retention-days] [[ml-put-job-example]] ==== {api-examples-title} -The following example creates the `total-requests` job: - [source,console] -------------------------------------------------- PUT _ml/anomaly_detectors/total-requests diff --git a/docs/reference/ml/anomaly-detection/apis/start-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/start-datafeed.asciidoc index dd3e6bbdfff54..2dffa5e62481e 100644 --- a/docs/reference/ml/anomaly-detection/apis/start-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/start-datafeed.asciidoc @@ -95,16 +95,14 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=datafeed-id] [[ml-start-datafeed-example]] ==== {api-examples-title} -The following example starts the `datafeed-it-ops-kpi` {dfeed}: - [source,console] -------------------------------------------------- -POST _ml/datafeeds/datafeed-total-requests/_start +POST _ml/datafeeds/datafeed-low_request_rate/_start { - "start": "2017-04-07T18:22:16Z" + "start": "2019-04-07T18:22:16Z" } -------------------------------------------------- -// TEST[skip:setup:server_metrics_openjob] +// TEST[skip:Kibana sample data] When the {dfeed} starts, you receive the following results: diff --git a/docs/reference/ml/anomaly-detection/apis/stop-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/stop-datafeed.asciidoc index f115d8657f7ec..21f46cef0b740 100644 --- a/docs/reference/ml/anomaly-detection/apis/stop-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/stop-datafeed.asciidoc @@ -70,16 +70,14 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-datafeeds] [[ml-stop-datafeed-example]] ==== {api-examples-title} -The following example stops the `datafeed-total-requests` {dfeed}: - [source,console] -------------------------------------------------- -POST _ml/datafeeds/datafeed-total-requests/_stop +POST _ml/datafeeds/datafeed-low_request_rate/_stop { "timeout": "30s" } -------------------------------------------------- -// TEST[skip:setup:server_metrics_startdf] +// TEST[skip:Kibana sample data] When the {dfeed} stops, you receive the following results: diff --git a/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc index 1336f71fcff77..044c65f11abcf 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc @@ -104,9 +104,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=scroll-size] [[ml-update-datafeed-example]] ==== {api-examples-title} -The following example updates the query for the `datafeed-total-requests` -{dfeed} so that only log entries of error level are analyzed: - [source,console] -------------------------------------------------- POST _ml/datafeeds/datafeed-total-requests/_update @@ -120,7 +117,6 @@ POST _ml/datafeeds/datafeed-total-requests/_update -------------------------------------------------- // TEST[skip:setup:server_metrics_datafeed] - When the {dfeed} is updated, you receive the full {dfeed} configuration with with the updated values: diff --git a/docs/reference/ml/anomaly-detection/apis/update-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-filter.asciidoc index a4aa5c3cab155..bf55d6900b1ec 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-filter.asciidoc @@ -41,9 +41,6 @@ Updates the description of a filter, adds items, or removes items. [[ml-update-filter-example]] ==== {api-examples-title} -You can change the description, add and remove items to the `safe_domains` -filter as follows: - [source,console] -------------------------------------------------- POST _ml/filters/safe_domains/_update diff --git a/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc index dff75da52f841..1fd3a6e620379 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-job.asciidoc @@ -162,8 +162,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=results-retention-days] [[ml-update-job-example]] ==== {api-examples-title} -The following example updates the `total-requests` job: - [source,console] -------------------------------------------------- POST _ml/anomaly_detectors/total-requests/_update diff --git a/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc index e0220042eccb0..da2bdfa3d3eab 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-snapshot.asciidoc @@ -48,8 +48,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=retain] [[ml-update-snapshot-example]] ==== {api-examples-title} -The following example updates the snapshot identified as `1491852978`: - [source,console] -------------------------------------------------- POST diff --git a/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc b/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc index 247eb32b10dc7..8c24c8e13a80c 100644 --- a/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/validate-detector.asciidoc @@ -107,8 +107,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=use-null] [[ml-valid-detector-example]] ==== {api-examples-title} -The following example validates detector configuration information: - [source,console] -------------------------------------------------- POST _ml/anomaly_detectors/_validate/detector diff --git a/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc index 750af88339c20..c74d46421dbf0 100644 --- a/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/validate-job.asciidoc @@ -35,8 +35,6 @@ see <>. [[ml-valid-job-example]] ==== {api-examples-title} -The following example validates job configuration information: - [source,console] -------------------------------------------------- POST _ml/anomaly_detectors/_validate From 9908f02dc9925cc8910002307943df0b2a6e3cf2 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Thu, 2 Jan 2020 11:14:47 +0000 Subject: [PATCH 349/686] Remove CreateIndexRequest.addMapping(type, string, xcontenttype) (#50419) We still have a number of places, mainly in test code but some in production, that are building mappings with a named type as the root of a map. CreateIndexRequest handles this automatically, but PutMappingRequest does not, which is a bit trappy - we can get situations like #50359 where the same mapping will work when an index is created but fail on an update. This commit is a first step to removing the leniency in CreateIndexRequest so that we can catch mappings with a named type root earlier. Relates to #41059 --- .../indices/create/CreateIndexRequest.java | 11 -------- .../create/CreateIndexRequestBuilder.java | 6 ++-- .../create/CreateIndexRequestTests.java | 28 ++----------------- .../action/admin/indices/get/GetIndexIT.java | 3 +- .../cluster/SpecificMasterNodesIT.java | 7 ++--- .../gateway/GatewayIndexStateIT.java | 7 ++--- .../gateway/RecoveryFromGatewayIT.java | 13 ++++----- .../org/elasticsearch/get/GetActionIT.java | 5 ++-- .../mapper/CopyToMapperIntegrationIT.java | 7 ++--- .../mapper/GeoPointFieldMapperTests.java | 6 ++-- .../index/mapper/MapperServiceTests.java | 5 ++-- .../mapping/ConcurrentDynamicTemplateIT.java | 8 ++---- .../mapping/UpdateMappingIntegrationIT.java | 8 +++--- .../RandomExceptionCircuitBreakerIT.java | 5 +--- .../indices/state/OpenCloseIndexIT.java | 9 ++---- .../indices/stats/IndexStatsIT.java | 6 ++-- .../routing/PartitionedRoutingIT.java | 5 ++-- .../aggregations/bucket/DateHistogramIT.java | 7 ++--- .../bucket/TermsDocCountErrorIT.java | 3 +- .../basic/SearchWithRandomExceptionsIT.java | 8 ++---- .../basic/SearchWithRandomIOExceptionsIT.java | 7 ++--- .../highlight/HighlighterSearchIT.java | 13 ++++----- .../elasticsearch/search/geo/GeoFilterIT.java | 4 +-- .../search/geo/GeoShapeIntegrationIT.java | 22 +++++++-------- .../search/geo/GeoShapeQueryTests.java | 28 +++++++++---------- .../geo/LegacyGeoShapeIntegrationIT.java | 18 ++++++------ .../search/morelikethis/MoreLikeThisIT.java | 28 +++---------------- .../search/nested/SimpleNestedIT.java | 10 +++---- .../search/query/SimpleQueryStringIT.java | 8 ++---- .../search/slice/SearchSliceIT.java | 7 ++--- .../search/sort/SimpleSortIT.java | 9 ++---- .../MachineLearningLicensingTests.java | 23 +++++++-------- .../support/SecurityIndexManager.java | 2 +- .../throttler/ActionThrottleTests.java | 4 +-- 34 files changed, 116 insertions(+), 224 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java index 39014d173c468..8ebf54d81b7c3 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java @@ -231,17 +231,6 @@ public CreateIndexRequest mapping(String mapping) { return this; } - /** - * Adds mapping that will be added when the index gets created. - * - * @param type The mapping type - * @param source The mapping source - * @param xContentType The content type of the source - */ - public CreateIndexRequest mapping(String type, String source, XContentType xContentType) { - return mapping(type, new BytesArray(source), xContentType); - } - /** * Adds mapping that will be added when the index gets created. * diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestBuilder.java index 93b4184f958bc..f1009f731d671 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestBuilder.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestBuilder.java @@ -96,12 +96,10 @@ public CreateIndexRequestBuilder setSettings(Map source) { /** * Adds mapping that will be added when the index gets created. * - * @param type The mapping type * @param source The mapping source - * @param xContentType The content type of the source */ - public CreateIndexRequestBuilder addMapping(String type, String source, XContentType xContentType) { - request.mapping(type, source, xContentType); + public CreateIndexRequestBuilder setMapping(String source) { + request.mapping(source); return this; } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestTests.java index c3d8a1853c1f4..845e645079fc9 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestTests.java @@ -22,7 +22,6 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; @@ -44,8 +43,8 @@ public class CreateIndexRequestTests extends ESTestCase { public void testSerialization() throws IOException { CreateIndexRequest request = new CreateIndexRequest("foo"); - String mapping = Strings.toString(JsonXContent.contentBuilder().startObject().startObject("my_type").endObject().endObject()); - request.mapping("my_type", mapping, XContentType.JSON); + String mapping = Strings.toString(JsonXContent.contentBuilder().startObject().startObject("_doc").endObject().endObject()); + request.mapping(mapping); try (BytesStreamOutput output = new BytesStreamOutput()) { request.writeTo(output); @@ -117,29 +116,6 @@ public void testMappingKeyedByType() throws IOException { request2.mapping("type1", builder); assertEquals(request1.mappings(), request2.mappings()); } - { - request1 = new CreateIndexRequest("foo"); - request2 = new CreateIndexRequest("bar"); - String nakedMapping = "{\"properties\": {\"foo\": {\"type\": \"integer\"}}}"; - request1.mapping("type2", nakedMapping, XContentType.JSON); - request2.mapping("type2", "{\"type2\": " + nakedMapping + "}", XContentType.JSON); - assertEquals(request1.mappings(), request2.mappings()); - } - { - request1 = new CreateIndexRequest("foo"); - request2 = new CreateIndexRequest("bar"); - Map nakedMapping = MapBuilder.newMapBuilder() - .put("properties", MapBuilder.newMapBuilder() - .put("bar", MapBuilder.newMapBuilder() - .put("type", "scaled_float") - .put("scaling_factor", 100) - .map()) - .map()) - .map(); - request1.mapping("type3", nakedMapping); - request2.mapping("type3", MapBuilder.newMapBuilder().put("type3", nakedMapping).map()); - assertEquals(request1.mappings(), request2.mappings()); - } } public void testSettingsType() throws IOException { diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexIT.java b/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexIT.java index 5726e405f613b..dc269fd2386fe 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexIT.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexIT.java @@ -27,7 +27,6 @@ import org.elasticsearch.cluster.metadata.MappingMetaData; import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.test.ESIntegTestCase; @@ -51,7 +50,7 @@ public class GetIndexIT extends ESIntegTestCase { @Override protected void setupSuiteScopeCluster() throws Exception { - assertAcked(prepareCreate("idx").addAlias(new Alias("alias_idx")).addMapping("type1", "{\"type1\":{}}", XContentType.JSON) + assertAcked(prepareCreate("idx").addAlias(new Alias("alias_idx")) .setSettings(Settings.builder().put("number_of_shards", 1)).get()); ensureSearchable("idx"); createIndex("empty_idx"); diff --git a/server/src/test/java/org/elasticsearch/cluster/SpecificMasterNodesIT.java b/server/src/test/java/org/elasticsearch/cluster/SpecificMasterNodesIT.java index cad4f51d7eac0..0e8f0f3f7e193 100644 --- a/server/src/test/java/org/elasticsearch/cluster/SpecificMasterNodesIT.java +++ b/server/src/test/java/org/elasticsearch/cluster/SpecificMasterNodesIT.java @@ -23,7 +23,6 @@ import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclusionsAction; import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclusionsRequest; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.discovery.MasterNotDiscoveredException; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.node.Node; @@ -141,9 +140,9 @@ public void testAliasFilterValidation() { internalCluster().startNode(Settings.builder() .put(Node.NODE_DATA_SETTING.getKey(), true).put(Node.NODE_MASTER_SETTING.getKey(), false)); - assertAcked(prepareCreate("test").addMapping( - "type1", "{\"type1\" : {\"properties\" : {\"table_a\" : { \"type\" : \"nested\", " + - "\"properties\" : {\"field_a\" : { \"type\" : \"keyword\" },\"field_b\" :{ \"type\" : \"keyword\" }}}}}}", XContentType.JSON)); + assertAcked(prepareCreate("test").setMapping( + "{\"properties\" : {\"table_a\" : { \"type\" : \"nested\", " + + "\"properties\" : {\"field_a\" : { \"type\" : \"keyword\" },\"field_b\" :{ \"type\" : \"keyword\" }}}}}")); client().admin().indices().prepareAliases().addAlias("test", "a_test", QueryBuilders.nestedQuery("table_a", QueryBuilders.termQuery("table_a.field_b", "y"), ScoreMode.Avg)).get(); } diff --git a/server/src/test/java/org/elasticsearch/gateway/GatewayIndexStateIT.java b/server/src/test/java/org/elasticsearch/gateway/GatewayIndexStateIT.java index 1df326cb1857e..1f736eaa3f74e 100644 --- a/server/src/test/java/org/elasticsearch/gateway/GatewayIndexStateIT.java +++ b/server/src/test/java/org/elasticsearch/gateway/GatewayIndexStateIT.java @@ -46,7 +46,6 @@ import org.elasticsearch.common.Priority; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.mapper.MapperParsingException; @@ -424,16 +423,14 @@ public void testRecoverMissingAnalyzer() throws Exception { prepareCreate("test").setSettings(Settings.builder() .put("index.analysis.analyzer.test.tokenizer", "standard") .put("index.number_of_shards", "1")) - .addMapping("type1", "{\n" + - " \"type1\": {\n" + + .setMapping("{\n" + " \"properties\": {\n" + " \"field1\": {\n" + " \"type\": \"text\",\n" + " \"analyzer\": \"test\"\n" + " }\n" + " }\n" + - " }\n" + - " }}", XContentType.JSON).get(); + " }}").get(); logger.info("--> indexing a simple document"); client().prepareIndex("test").setId("1").setSource("field1", "value one").setRefreshPolicy(IMMEDIATE).get(); logger.info("--> waiting for green status"); diff --git a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java index e3a754f0003db..ee55545fe34ea 100644 --- a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java +++ b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java @@ -37,7 +37,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexService; @@ -98,10 +97,10 @@ public void testOneNodeRecoverFromGateway() throws Exception { internalCluster().startNode(); - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1") - .startObject("properties").startObject("appAccountIds").field("type", "text").endObject().endObject() + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() + .startObject("properties").startObject("appAccountIds").field("type", "text").endObject() .endObject().endObject()); - assertAcked(prepareCreate("test").addMapping("type1", mapping, XContentType.JSON)); + assertAcked(prepareCreate("test").setMapping(mapping)); client().prepareIndex("test").setId("10990239").setSource(jsonBuilder().startObject() .startArray("appAccountIds").value(14).value(179).endArray().endObject()).execute().actionGet(); @@ -168,14 +167,14 @@ private Map assertAndCapturePrimaryTerms(Map pre public void testSingleNodeNoFlush() throws Exception { internalCluster().startNode(); - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1") + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() .startObject("properties").startObject("field").field("type", "text").endObject().startObject("num").field("type", "integer") - .endObject().endObject() + .endObject() .endObject().endObject()); // note: default replica settings are tied to #data nodes-1 which is 0 here. We can do with 1 in this test. int numberOfShards = numberOfShards(); assertAcked(prepareCreate("test").setSettings(Settings.builder().put(SETTING_NUMBER_OF_SHARDS, numberOfShards()) - .put(SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 1))).addMapping("type1", mapping, XContentType.JSON)); + .put(SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 1))).setMapping(mapping)); int value1Docs; int value2Docs; diff --git a/server/src/test/java/org/elasticsearch/get/GetActionIT.java b/server/src/test/java/org/elasticsearch/get/GetActionIT.java index 13a64d66f744b..55e706601ddc4 100644 --- a/server/src/test/java/org/elasticsearch/get/GetActionIT.java +++ b/server/src/test/java/org/elasticsearch/get/GetActionIT.java @@ -271,13 +271,12 @@ public void testSimpleMultiGet() throws Exception { } public void testGetDocWithMultivaluedFields() throws Exception { - String mapping1 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1") + String mapping1 = Strings.toString(XContentFactory.jsonBuilder().startObject() .startObject("properties") .startObject("field").field("type", "text").field("store", true).endObject() - .endObject() .endObject().endObject()); assertAcked(prepareCreate("test") - .addMapping("type1", mapping1, XContentType.JSON)); + .setMapping(mapping1)); ensureGreen(); GetResponse response = client().prepareGet("test", "1").get(); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/CopyToMapperIntegrationIT.java b/server/src/test/java/org/elasticsearch/index/mapper/CopyToMapperIntegrationIT.java index 5cad69fbb112b..e91f216a76ea0 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/CopyToMapperIntegrationIT.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/CopyToMapperIntegrationIT.java @@ -23,7 +23,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregator.SubAggCollectionMode; @@ -70,15 +69,15 @@ public void testDynamicTemplateCopyTo() throws Exception { } public void testDynamicObjectCopyTo() throws Exception { - String mapping = Strings.toString(jsonBuilder().startObject().startObject("_doc").startObject("properties") + String mapping = Strings.toString(jsonBuilder().startObject().startObject("properties") .startObject("foo") .field("type", "text") .field("copy_to", "root.top.child") .endObject() - .endObject().endObject().endObject()); + .endObject().endObject()); assertAcked( client().admin().indices().prepareCreate("test-idx") - .addMapping("_doc", mapping, XContentType.JSON) + .setMapping(mapping) ); client().prepareIndex("test-idx").setId("1") .setSource("foo", "bar") diff --git a/server/src/test/java/org/elasticsearch/index/mapper/GeoPointFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/GeoPointFieldMapperTests.java index 3ff6eda1fd7fa..b1420d712f860 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/GeoPointFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/GeoPointFieldMapperTests.java @@ -337,16 +337,16 @@ public void testIgnoreZValue() throws IOException { public void testMultiField() throws Exception { int numDocs = randomIntBetween(10, 100); - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("pin") + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() .startObject("properties").startObject("location") .field("type", "geo_point") .startObject("fields") .startObject("geohash").field("type", "keyword").endObject() // test geohash as keyword .startObject("latlon").field("type", "keyword").endObject() // test geohash as string .endObject() - .endObject().endObject().endObject().endObject()); + .endObject().endObject().endObject()); CreateIndexRequestBuilder mappingRequest = client().admin().indices().prepareCreate("test") - .addMapping("pin", mapping, XContentType.JSON); + .setMapping(mapping); mappingRequest.execute().actionGet(); // create index and add random test points diff --git a/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java b/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java index 4ac246561e6eb..d934040ab6ed8 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java @@ -27,7 +27,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.IndexSettings; @@ -148,7 +147,7 @@ public void testPartitionedConstraints() { // partitioned index must have routing IllegalArgumentException noRoutingException = expectThrows(IllegalArgumentException.class, () -> { client().admin().indices().prepareCreate("test-index") - .addMapping("type", "{\"type\":{}}", XContentType.JSON) + .setMapping("{\"_doc\":{}}") .setSettings(Settings.builder() .put("index.number_of_shards", 4) .put("index.routing_partition_size", 2)) @@ -158,7 +157,7 @@ public void testPartitionedConstraints() { // valid partitioned index assertTrue(client().admin().indices().prepareCreate("test-index") - .addMapping("type", "{\"type\":{\"_routing\":{\"required\":true}}}", XContentType.JSON) + .setMapping("{\"_doc\":{\"_routing\":{\"required\":true}}}") .setSettings(Settings.builder() .put("index.number_of_shards", 4) .put("index.routing_partition_size", 2)) diff --git a/server/src/test/java/org/elasticsearch/indices/mapping/ConcurrentDynamicTemplateIT.java b/server/src/test/java/org/elasticsearch/indices/mapping/ConcurrentDynamicTemplateIT.java index b62b80854b57a..a1944225b0b85 100644 --- a/server/src/test/java/org/elasticsearch/indices/mapping/ConcurrentDynamicTemplateIT.java +++ b/server/src/test/java/org/elasticsearch/indices/mapping/ConcurrentDynamicTemplateIT.java @@ -22,7 +22,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.index.IndexResponse; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.test.ESIntegTestCase; @@ -37,15 +36,14 @@ import static org.hamcrest.Matchers.emptyIterable; public class ConcurrentDynamicTemplateIT extends ESIntegTestCase { - private final String mappingType = "test-mapping"; // see #3544 public void testConcurrentDynamicMapping() throws Exception { final String fieldName = "field"; - final String mapping = "{ \"" + mappingType + "\": {" + + final String mapping = "{" + "\"dynamic_templates\": [" + "{ \"" + fieldName + "\": {" + "\"path_match\": \"*\"," + "\"mapping\": {" + "\"type\": \"text\"," + "\"store\": true," - + "\"analyzer\": \"whitespace\" } } } ] } }"; + + "\"analyzer\": \"whitespace\" } } } ] }"; // The 'fieldNames' array is used to help with retrieval of index terms // after testing @@ -53,7 +51,7 @@ public void testConcurrentDynamicMapping() throws Exception { for (int i = 0; i < iters; i++) { cluster().wipeIndices("test"); assertAcked(prepareCreate("test") - .addMapping(mappingType, mapping, XContentType.JSON)); + .setMapping(mapping)); int numDocs = scaledRandomIntBetween(10, 100); final CountDownLatch latch = new CountDownLatch(numDocs); final List throwable = new CopyOnWriteArrayList<>(); diff --git a/server/src/test/java/org/elasticsearch/indices/mapping/UpdateMappingIntegrationIT.java b/server/src/test/java/org/elasticsearch/indices/mapping/UpdateMappingIntegrationIT.java index 0929beb189213..0feb82afc1fb1 100644 --- a/server/src/test/java/org/elasticsearch/indices/mapping/UpdateMappingIntegrationIT.java +++ b/server/src/test/java/org/elasticsearch/indices/mapping/UpdateMappingIntegrationIT.java @@ -117,7 +117,7 @@ public void testUpdateMappingWithoutType() { Settings.builder() .put("index.number_of_shards", 1) .put("index.number_of_replicas", 0) - ).addMapping("_doc", "{\"_doc\":{\"properties\":{\"body\":{\"type\":\"text\"}}}}", XContentType.JSON) + ).setMapping("{\"properties\":{\"body\":{\"type\":\"text\"}}}") .execute().actionGet(); client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus().execute().actionGet(); @@ -158,7 +158,7 @@ public void testUpdateMappingWithConflicts() { Settings.builder() .put("index.number_of_shards", 2) .put("index.number_of_replicas", 0) - ).addMapping("type", "{\"type\":{\"properties\":{\"body\":{\"type\":\"text\"}}}}", XContentType.JSON) + ).setMapping("{\"properties\":{\"body\":{\"type\":\"text\"}}}") .execute().actionGet(); client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus().execute().actionGet(); @@ -173,7 +173,7 @@ public void testUpdateMappingWithConflicts() { public void testUpdateMappingWithNormsConflicts() { client().admin().indices().prepareCreate("test") - .addMapping("type", "{\"type\":{\"properties\":{\"body\":{\"type\":\"text\", \"norms\": false }}}}", XContentType.JSON) + .setMapping("{\"properties\":{\"body\":{\"type\":\"text\", \"norms\": false }}}") .execute().actionGet(); try { client().admin().indices().preparePutMapping("test") @@ -194,7 +194,7 @@ public void testUpdateMappingNoChanges() { Settings.builder() .put("index.number_of_shards", 2) .put("index.number_of_replicas", 0) - ).addMapping("type", "{\"type\":{\"properties\":{\"body\":{\"type\":\"text\"}}}}", XContentType.JSON) + ).setMapping("{\"properties\":{\"body\":{\"type\":\"text\"}}}") .execute().actionGet(); client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus().execute().actionGet(); diff --git a/server/src/test/java/org/elasticsearch/indices/memory/breaker/RandomExceptionCircuitBreakerIT.java b/server/src/test/java/org/elasticsearch/indices/memory/breaker/RandomExceptionCircuitBreakerIT.java index 33260878ebda6..1fb278ba31311 100644 --- a/server/src/test/java/org/elasticsearch/indices/memory/breaker/RandomExceptionCircuitBreakerIT.java +++ b/server/src/test/java/org/elasticsearch/indices/memory/breaker/RandomExceptionCircuitBreakerIT.java @@ -36,7 +36,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.MockEngineFactoryPlugin; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.indices.IndicesService; @@ -81,7 +80,6 @@ public void testBreakerWithRandomExceptions() throws IOException, InterruptedExc String mapping = Strings // {} .toString(XContentFactory.jsonBuilder() .startObject() - .startObject("type") .startObject("properties") .startObject("test-str") .field("type", "keyword") @@ -92,7 +90,6 @@ public void testBreakerWithRandomExceptions() throws IOException, InterruptedExc .field("type", randomFrom(Arrays.asList("float", "long", "double", "short", "integer"))) .endObject() // test-num .endObject() // properties - .endObject() // type .endObject()); final double topLevelRate; final double lowLevelRate; @@ -123,7 +120,7 @@ public void testBreakerWithRandomExceptions() throws IOException, InterruptedExc logger.info("creating index: [test] using settings: [{}]", settings.build()); CreateIndexResponse response = client().admin().indices().prepareCreate("test") .setSettings(settings) - .addMapping("type", mapping, XContentType.JSON).execute().actionGet(); + .setMapping(mapping).execute().actionGet(); final int numDocs; if (response.isShardsAcknowledged() == false) { /* some seeds just won't let you create the index at all and we enter a ping-pong mode diff --git a/server/src/test/java/org/elasticsearch/indices/state/OpenCloseIndexIT.java b/server/src/test/java/org/elasticsearch/indices/state/OpenCloseIndexIT.java index a36e4dae66958..c95b61f7e6a90 100644 --- a/server/src/test/java/org/elasticsearch/indices/state/OpenCloseIndexIT.java +++ b/server/src/test/java/org/elasticsearch/indices/state/OpenCloseIndexIT.java @@ -33,7 +33,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.query.QueryBuilders; @@ -266,17 +265,15 @@ public void testOpenWaitingForActiveShardsFailed() throws Exception { public void testOpenCloseWithDocs() throws IOException, ExecutionException, InterruptedException { String mapping = Strings.toString(XContentFactory.jsonBuilder(). startObject(). - startObject("type"). startObject("properties"). startObject("test") .field("type", "keyword") - .endObject(). - endObject(). - endObject() + .endObject() + .endObject() .endObject()); assertAcked(client().admin().indices().prepareCreate("test") - .addMapping("type", mapping, XContentType.JSON)); + .setMapping(mapping)); ensureGreen(); int docs = between(10, 100); IndexRequestBuilder[] builder = new IndexRequestBuilder[docs]; diff --git a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java index 70eb62e127c75..c84272127623f 100644 --- a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java +++ b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java @@ -773,11 +773,9 @@ public void testMultiIndex() throws Exception { public void testCompletionFieldsParam() throws Exception { assertAcked(prepareCreate("test1") - .addMapping( - "_doc", + .setMapping( "{ \"properties\": { \"bar\": { \"type\": \"text\", \"fields\": { \"completion\": { \"type\": \"completion\" }}}" + - ",\"baz\": { \"type\": \"text\", \"fields\": { \"completion\": { \"type\": \"completion\" }}}}}", - XContentType.JSON)); + ",\"baz\": { \"type\": \"text\", \"fields\": { \"completion\": { \"type\": \"completion\" }}}}}")); ensureGreen(); client().prepareIndex("test1").setId(Integer.toString(1)).setSource("{\"bar\":\"bar\",\"baz\":\"baz\"}" diff --git a/server/src/test/java/org/elasticsearch/routing/PartitionedRoutingIT.java b/server/src/test/java/org/elasticsearch/routing/PartitionedRoutingIT.java index 45a1e98289dbe..a837e6441256c 100644 --- a/server/src/test/java/org/elasticsearch/routing/PartitionedRoutingIT.java +++ b/server/src/test/java/org/elasticsearch/routing/PartitionedRoutingIT.java @@ -23,7 +23,6 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.test.ESIntegTestCase; import org.mockito.internal.util.collections.Sets; @@ -45,7 +44,7 @@ public void testVariousPartitionSizes() throws Exception { .put("index.number_of_shards", shards) .put("index.number_of_routing_shards", shards) .put("index.routing_partition_size", partitionSize)) - .addMapping("type", "{\"type\":{\"_routing\":{\"required\":true}}}", XContentType.JSON) + .setMapping("{\"_routing\":{\"required\":true}}") .execute().actionGet(); ensureGreen(); @@ -74,7 +73,7 @@ public void testShrinking() throws Exception { .put("index.number_of_routing_shards", currentShards) .put("index.number_of_replicas", numberOfReplicas()) .put("index.routing_partition_size", partitionSize)) - .addMapping("_doc", "{\"_doc\":{\"_routing\":{\"required\":true}}}", XContentType.JSON) + .setMapping("{\"_routing\":{\"required\":true}}") .execute().actionGet(); ensureGreen(); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java index 0dceff9d0f0d1..19c48c9b32f9d 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java @@ -28,7 +28,6 @@ import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.time.DateMathParser; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.index.query.MatchNoneQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; @@ -1238,10 +1237,10 @@ public void testSingleValueFieldWithExtendedBoundsOffset() throws Exception { public void testSingleValueWithMultipleDateFormatsFromMapping() throws Exception { String mappingJson = Strings.toString(jsonBuilder().startObject() - .startObject("type").startObject("properties") + .startObject("properties") .startObject("date").field("type", "date").field("format", "strict_date_optional_time||dd-MM-yyyy") - .endObject().endObject().endObject().endObject()); - prepareCreate("idx2").addMapping("type", mappingJson, XContentType.JSON).get(); + .endObject().endObject().endObject()); + prepareCreate("idx2").setMapping(mappingJson).get(); IndexRequestBuilder[] reqs = new IndexRequestBuilder[5]; for (int i = 0; i < reqs.length; i++) { reqs[i] = client().prepareIndex("idx2").setId("" + i) diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/TermsDocCountErrorIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/TermsDocCountErrorIT.java index b6b817cc7c270..f405f47e0ee2c 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/TermsDocCountErrorIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/TermsDocCountErrorIT.java @@ -23,7 +23,6 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.search.aggregations.Aggregator.SubAggCollectionMode; import org.elasticsearch.search.aggregations.BucketOrder; import org.elasticsearch.search.aggregations.bucket.terms.Terms; @@ -89,7 +88,7 @@ public void setupSuiteScopeCluster() throws Exception { } numRoutingValues = between(1,40); assertAcked(prepareCreate("idx_with_routing") - .addMapping("type", "{ \"type\" : { \"_routing\" : { \"required\" : true } } }", XContentType.JSON)); + .setMapping("{ \"_routing\" : { \"required\" : true } }")); for (int i = 0; i < numDocs; i++) { builders.add(client().prepareIndex("idx_single_shard").setId("" + i) .setRouting(String.valueOf(randomInt(numRoutingValues))) diff --git a/server/src/test/java/org/elasticsearch/search/basic/SearchWithRandomExceptionsIT.java b/server/src/test/java/org/elasticsearch/search/basic/SearchWithRandomExceptionsIT.java index c06b501f08681..593423e843cd7 100644 --- a/server/src/test/java/org/elasticsearch/search/basic/SearchWithRandomExceptionsIT.java +++ b/server/src/test/java/org/elasticsearch/search/basic/SearchWithRandomExceptionsIT.java @@ -36,7 +36,6 @@ import org.elasticsearch.common.settings.Settings.Builder; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.MockEngineFactoryPlugin; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.plugins.Plugin; @@ -70,13 +69,10 @@ protected boolean addMockInternalEngine() { public void testRandomExceptions() throws IOException, InterruptedException, ExecutionException { String mapping = Strings.toString(XContentFactory.jsonBuilder(). startObject(). - startObject("type"). startObject("properties"). startObject("test") .field("type", "keyword") - .endObject(). - endObject(). - endObject() + .endObject().endObject() .endObject()); final double lowLevelRate; final double topLevelRate; @@ -107,7 +103,7 @@ public void testRandomExceptions() throws IOException, InterruptedException, Exe logger.info("creating index: [test] using settings: [{}]", settings.build()); assertAcked(prepareCreate("test") .setSettings(settings) - .addMapping("type", mapping, XContentType.JSON)); + .setMapping(mapping)); ensureSearchable(); final int numDocs = between(10, 100); int numCreated = 0; diff --git a/server/src/test/java/org/elasticsearch/search/basic/SearchWithRandomIOExceptionsIT.java b/server/src/test/java/org/elasticsearch/search/basic/SearchWithRandomIOExceptionsIT.java index 265fdef034d8c..e116fbe380fcf 100644 --- a/server/src/test/java/org/elasticsearch/search/basic/SearchWithRandomIOExceptionsIT.java +++ b/server/src/test/java/org/elasticsearch/search/basic/SearchWithRandomIOExceptionsIT.java @@ -32,7 +32,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.sort.SortOrder; @@ -61,12 +60,10 @@ public void testRemoveWhenAwaitsFixIsResolved() { public void testRandomDirectoryIOExceptions() throws IOException, InterruptedException, ExecutionException { String mapping = Strings.toString(XContentFactory.jsonBuilder(). startObject(). - startObject("type"). startObject("properties"). startObject("test") .field("type", "keyword") .endObject(). - endObject(). endObject() .endObject()); final double exceptionRate; @@ -98,7 +95,7 @@ public void testRandomDirectoryIOExceptions() throws IOException, InterruptedExc logger.info("creating index: [test] using settings: [{}]", settings.build()); client().admin().indices().prepareCreate("test") .setSettings(settings) - .addMapping("type", mapping, XContentType.JSON).get(); + .setMapping(mapping).get(); numInitialDocs = between(10, 100); ensureGreen(); for (int i = 0; i < numInitialDocs; i++) { @@ -121,7 +118,7 @@ public void testRandomDirectoryIOExceptions() throws IOException, InterruptedExc logger.info("creating index: [test] using settings: [{}]", settings.build()); client().admin().indices().prepareCreate("test") .setSettings(settings) - .addMapping("type", mapping, XContentType.JSON).get(); + .setMapping(mapping).get(); } ClusterHealthResponse clusterHealthResponse = client().admin().cluster() // it's OK to timeout here diff --git a/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java b/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java index 68077456a6c5f..d09e7d05fa6f0 100644 --- a/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java @@ -37,7 +37,6 @@ import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.analysis.AbstractIndexAnalyzerProvider; import org.elasticsearch.index.analysis.AnalyzerProvider; import org.elasticsearch.index.analysis.PreConfiguredTokenFilter; @@ -2722,7 +2721,7 @@ public void testKeywordFieldHighlighting() throws IOException { } public void testACopyFieldWithNestedQuery() throws Exception { - String mapping = Strings.toString(jsonBuilder().startObject().startObject("type").startObject("properties") + String mapping = Strings.toString(jsonBuilder().startObject().startObject("properties") .startObject("foo") .field("type", "nested") .startObject("properties") @@ -2737,8 +2736,8 @@ public void testACopyFieldWithNestedQuery() throws Exception { .field("term_vector", "with_positions_offsets") .field("store", true) .endObject() - .endObject().endObject().endObject()); - prepareCreate("test").addMapping("type", mapping, XContentType.JSON).get(); + .endObject().endObject()); + prepareCreate("test").setMapping(mapping).get(); client().prepareIndex("test").setId("1").setSource(jsonBuilder().startObject().startArray("foo") .startObject().field("text", "brown").endObject() @@ -2833,7 +2832,7 @@ public void testHighlightQueryRewriteDatesWithNow() throws Exception { } public void testWithNestedQuery() throws Exception { - String mapping = Strings.toString(jsonBuilder().startObject().startObject("type").startObject("properties") + String mapping = Strings.toString(jsonBuilder().startObject().startObject("properties") .startObject("text") .field("type", "text") .field("index_options", "offsets") @@ -2847,8 +2846,8 @@ public void testWithNestedQuery() throws Exception { .endObject() .endObject() .endObject() - .endObject().endObject().endObject()); - prepareCreate("test").addMapping("type", mapping, XContentType.JSON).get(); + .endObject().endObject()); + prepareCreate("test").setMapping(mapping).get(); client().prepareIndex("test").setId("1").setSource(jsonBuilder().startObject() .startArray("foo") diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoFilterIT.java b/server/src/test/java/org/elasticsearch/search/geo/GeoFilterIT.java index 5a8f32313557d..0ce547ec14975 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoFilterIT.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoFilterIT.java @@ -203,18 +203,16 @@ public void testShapeRelations() throws Exception { String mapping = Strings.toString(XContentFactory.jsonBuilder() .startObject() - .startObject("polygon") .startObject("properties") .startObject("area") .field("type", "geo_shape") .field("tree", "geohash") .endObject() .endObject() - .endObject() .endObject()); CreateIndexRequestBuilder mappingRequest = client().admin().indices().prepareCreate("shapes") - .addMapping("polygon", mapping, XContentType.JSON); + .setMapping(mapping); mappingRequest.get(); client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus().get(); diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationIT.java b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationIT.java index bb42258ab8de6..fac3bda1c1935 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationIT.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationIT.java @@ -46,24 +46,24 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase { */ public void testOrientationPersistence() throws Exception { String idxName = "orientation"; - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("shape") + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() .startObject("properties").startObject("location") .field("type", "geo_shape") .field("orientation", "left") - .endObject().endObject() + .endObject() .endObject().endObject()); // create index - assertAcked(prepareCreate(idxName).addMapping("shape", mapping, XContentType.JSON)); + assertAcked(prepareCreate(idxName).setMapping(mapping)); - mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("shape") + mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() .startObject("properties").startObject("location") .field("type", "geo_shape") .field("orientation", "right") - .endObject().endObject() + .endObject() .endObject().endObject()); - assertAcked(prepareCreate(idxName+"2").addMapping("shape", mapping, XContentType.JSON)); + assertAcked(prepareCreate(idxName+"2").setMapping(mapping)); ensureGreen(idxName, idxName+"2"); internalCluster().fullRestart(); @@ -149,7 +149,7 @@ public void testMappingUpdate() throws Exception { * Test that the indexed shape routing can be provided if it is required */ public void testIndexShapeRouting() throws Exception { - String mapping = "{\n" + + String mapping = "{\"_doc\":{\n" + " \"_routing\": {\n" + " \"required\": true\n" + " },\n" + @@ -158,11 +158,11 @@ public void testIndexShapeRouting() throws Exception { " \"type\": \"geo_shape\"\n" + " }\n" + " }\n" + - " }"; + " }}"; // create index - assertAcked(client().admin().indices().prepareCreate("test").addMapping("doc", mapping, XContentType.JSON).get()); + assertAcked(client().admin().indices().prepareCreate("test").setMapping(mapping).get()); ensureGreen(); String source = "{\n" + @@ -201,10 +201,10 @@ public void testIndexPolygonDateLine() throws Exception { // create index - assertAcked(client().admin().indices().prepareCreate("vector").addMapping("doc", mappingVector, XContentType.JSON).get()); + assertAcked(client().admin().indices().prepareCreate("vector").setMapping(mappingVector).get()); ensureGreen(); - assertAcked(client().admin().indices().prepareCreate("quad").addMapping("doc", mappingQuad, XContentType.JSON).get()); + assertAcked(client().admin().indices().prepareCreate("quad").setMapping(mappingQuad).get()); ensureGreen(); String source = "{\n" + diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java index 8ed913242ebda..612973c0b2362 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java @@ -77,21 +77,21 @@ public class GeoShapeQueryTests extends ESSingleNodeTestCase { }; private XContentBuilder createMapping() throws Exception { - XContentBuilder xcb = XContentFactory.jsonBuilder().startObject().startObject("type1") + XContentBuilder xcb = XContentFactory.jsonBuilder().startObject() .startObject("properties").startObject("location") .field("type", "geo_shape"); if (randomBoolean()) { xcb = xcb.field("tree", randomFrom(PREFIX_TREES)) .field("strategy", randomFrom(SpatialStrategy.RECURSIVE, SpatialStrategy.TERM)); } - xcb = xcb.endObject().endObject().endObject().endObject(); + xcb = xcb.endObject().endObject().endObject(); return xcb; } public void testNullShape() throws Exception { String mapping = Strings.toString(createMapping()); - client().admin().indices().prepareCreate("test").addMapping("type1", mapping, XContentType.JSON).get(); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); ensureGreen(); client().prepareIndex("test").setId("aNullshape").setSource("{\"location\": null}", XContentType.JSON) @@ -102,7 +102,7 @@ public void testNullShape() throws Exception { public void testIndexPointsFilterRectangle() throws Exception { String mapping = Strings.toString(createMapping()); - client().admin().indices().prepareCreate("test").addMapping("type1", mapping, XContentType.JSON).get(); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); ensureGreen(); client().prepareIndex("test").setId("1").setSource(jsonBuilder().startObject() @@ -143,12 +143,12 @@ public void testIndexPointsFilterRectangle() throws Exception { } public void testEdgeCases() throws Exception { - XContentBuilder xcb = XContentFactory.jsonBuilder().startObject().startObject("type1") + XContentBuilder xcb = XContentFactory.jsonBuilder().startObject() .startObject("properties").startObject("location") .field("type", "geo_shape") - .endObject().endObject().endObject().endObject(); + .endObject().endObject().endObject(); String mapping = Strings.toString(xcb); - client().admin().indices().prepareCreate("test").addMapping("type1", mapping, XContentType.JSON).get(); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); ensureGreen(); client().prepareIndex("test").setId("blakely").setSource(jsonBuilder().startObject() @@ -180,7 +180,7 @@ public void testEdgeCases() throws Exception { public void testIndexedShapeReference() throws Exception { String mapping = Strings.toString(createMapping()); - client().admin().indices().prepareCreate("test").addMapping("type1", mapping, XContentType.JSON).get(); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); createIndex("shapes"); ensureGreen(); @@ -584,17 +584,17 @@ public void testShapeFilterWithDefinedGeoCollection() throws Exception { } public void testPointsOnly() throws Exception { - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1") + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() .startObject("properties").startObject("location") .field("type", "geo_shape") .field("tree", randomBoolean() ? "quadtree" : "geohash") .field("tree_levels", "6") .field("distance_error_pct", "0.01") .field("points_only", true) - .endObject().endObject() + .endObject() .endObject().endObject()); - client().admin().indices().prepareCreate("geo_points_only").addMapping("type1", mapping, XContentType.JSON).get(); + client().admin().indices().prepareCreate("geo_points_only").setMapping(mapping).get(); ensureGreen(); ShapeBuilder shape = RandomShapeGenerator.createShape(random()); @@ -617,17 +617,17 @@ public void testPointsOnly() throws Exception { } public void testPointsOnlyExplicit() throws Exception { - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1") + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() .startObject("properties").startObject("location") .field("type", "geo_shape") .field("tree", randomBoolean() ? "quadtree" : "geohash") .field("tree_levels", "6") .field("distance_error_pct", "0.01") .field("points_only", true) - .endObject().endObject() + .endObject() .endObject().endObject()); - client().admin().indices().prepareCreate("geo_points_only").addMapping("type1", mapping, XContentType.JSON).get(); + client().admin().indices().prepareCreate("geo_points_only").setMapping(mapping).get(); ensureGreen(); // MULTIPOINT diff --git a/server/src/test/java/org/elasticsearch/search/geo/LegacyGeoShapeIntegrationIT.java b/server/src/test/java/org/elasticsearch/search/geo/LegacyGeoShapeIntegrationIT.java index 1a919015bb401..4e7ded64a2586 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/LegacyGeoShapeIntegrationIT.java +++ b/server/src/test/java/org/elasticsearch/search/geo/LegacyGeoShapeIntegrationIT.java @@ -46,26 +46,26 @@ public class LegacyGeoShapeIntegrationIT extends ESIntegTestCase { */ public void testOrientationPersistence() throws Exception { String idxName = "orientation"; - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("shape") + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() .startObject("properties").startObject("location") .field("type", "geo_shape") .field("tree", "quadtree") .field("orientation", "left") - .endObject().endObject() + .endObject() .endObject().endObject()); // create index - assertAcked(prepareCreate(idxName).addMapping("shape", mapping, XContentType.JSON)); + assertAcked(prepareCreate(idxName).setMapping(mapping)); - mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("shape") + mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() .startObject("properties").startObject("location") .field("type", "geo_shape") .field("tree", "quadtree") .field("orientation", "right") - .endObject().endObject() + .endObject() .endObject().endObject()); - assertAcked(prepareCreate(idxName+"2").addMapping("shape", mapping, XContentType.JSON)); + assertAcked(prepareCreate(idxName+"2").setMapping(mapping)); ensureGreen(idxName, idxName+"2"); internalCluster().fullRestart(); @@ -130,7 +130,7 @@ public void testIgnoreMalformed() throws Exception { * Test that the indexed shape routing can be provided if it is required */ public void testIndexShapeRouting() throws Exception { - String mapping = "{\n" + + String mapping = "{\"_doc\":{\n" + " \"_routing\": {\n" + " \"required\": true\n" + " },\n" + @@ -140,11 +140,11 @@ public void testIndexShapeRouting() throws Exception { " \"tree\" : \"quadtree\"\n" + " }\n" + " }\n" + - " }"; + " }}"; // create index - assertAcked(client().admin().indices().prepareCreate("test").addMapping("doc", mapping, XContentType.JSON).get()); + assertAcked(client().admin().indices().prepareCreate("test").setMapping(mapping).get()); ensureGreen(); String source = "{\n" + diff --git a/server/src/test/java/org/elasticsearch/search/morelikethis/MoreLikeThisIT.java b/server/src/test/java/org/elasticsearch/search/morelikethis/MoreLikeThisIT.java index 99d83eb4521b1..34a9363d5a5b7 100644 --- a/server/src/test/java/org/elasticsearch/search/morelikethis/MoreLikeThisIT.java +++ b/server/src/test/java/org/elasticsearch/search/morelikethis/MoreLikeThisIT.java @@ -25,11 +25,9 @@ import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.cluster.health.ClusterHealthStatus; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.MoreLikeThisQueryBuilder; import org.elasticsearch.index.query.MoreLikeThisQueryBuilder.Item; import org.elasticsearch.index.query.QueryBuilder; @@ -223,13 +221,8 @@ public void testMoreLikeThisWithAliases() throws Exception { public void testMoreLikeThisWithAliasesInLikeDocuments() throws Exception { String indexName = "foo"; String aliasName = "foo_name"; - String typeName = "bar"; - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("bar") - .startObject("properties") - .endObject() - .endObject().endObject()); - client().admin().indices().prepareCreate(indexName).addMapping(typeName, mapping, XContentType.JSON).get(); + client().admin().indices().prepareCreate(indexName).get(); client().admin().indices().prepareAliases().addAlias(indexName, aliasName).get(); assertThat(ensureGreen(), equalTo(ClusterHealthStatus.GREEN)); @@ -249,11 +242,7 @@ public void testMoreLikeThisWithAliasesInLikeDocuments() throws Exception { } public void testMoreLikeThisIssue2197() throws Exception { - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("bar") - .startObject("properties") - .endObject() - .endObject().endObject()); - client().admin().indices().prepareCreate("foo").addMapping("bar", mapping, XContentType.JSON).get(); + client().admin().indices().prepareCreate("foo").get(); client().prepareIndex("foo").setId("1") .setSource(jsonBuilder().startObject().startObject("foo").field("bar", "boz").endObject().endObject()) .get(); @@ -272,11 +261,7 @@ public void testMoreLikeThisIssue2197() throws Exception { // Issue #2489 public void testMoreLikeWithCustomRouting() throws Exception { - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("bar") - .startObject("properties") - .endObject() - .endObject().endObject()); - client().admin().indices().prepareCreate("foo").addMapping("bar", mapping, XContentType.JSON).get(); + client().admin().indices().prepareCreate("foo").get(); ensureGreen(); client().prepareIndex("foo").setId("1") @@ -293,13 +278,8 @@ public void testMoreLikeWithCustomRouting() throws Exception { // Issue #3039 public void testMoreLikeThisIssueRoutingNotSerialized() throws Exception { - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("bar") - .startObject("properties") - .endObject() - .endObject().endObject()); assertAcked(prepareCreate("foo", 2, - Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 2).put(SETTING_NUMBER_OF_REPLICAS, 0)) - .addMapping("bar", mapping, XContentType.JSON)); + Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 2).put(SETTING_NUMBER_OF_REPLICAS, 0))); ensureGreen(); client().prepareIndex("foo").setId("1") diff --git a/server/src/test/java/org/elasticsearch/search/nested/SimpleNestedIT.java b/server/src/test/java/org/elasticsearch/search/nested/SimpleNestedIT.java index 75dca03b297c1..cca4d4e8e1a57 100644 --- a/server/src/test/java/org/elasticsearch/search/nested/SimpleNestedIT.java +++ b/server/src/test/java/org/elasticsearch/search/nested/SimpleNestedIT.java @@ -522,8 +522,7 @@ public void testSimpleNestedSortingWithNestedFilterMissing() throws Exception { public void testNestedSortWithMultiLevelFiltering() throws Exception { assertAcked(prepareCreate("test") - .addMapping("type1", "{\n" - + " \"type1\": {\n" + .setMapping("{\n" + " \"properties\": {\n" + " \"acl\": {\n" + " \"type\": \"nested\",\n" @@ -545,8 +544,7 @@ public void testNestedSortWithMultiLevelFiltering() throws Exception { + " }\n" + " }\n" + " }\n" - + " }\n" - + "}", XContentType.JSON)); + + "}")); ensureGreen(); client().prepareIndex("test").setId("1").setSource("{\n" @@ -738,7 +736,7 @@ public void testNestedSortWithMultiLevelFiltering() throws Exception { public void testLeakingSortValues() throws Exception { assertAcked(prepareCreate("test") .setSettings(Settings.builder().put("number_of_shards", 1)) - .addMapping("test-type", "{\n" + .setMapping("{\"_doc\":{\n" + " \"dynamic\": \"strict\",\n" + " \"properties\": {\n" + " \"nested1\": {\n" @@ -758,7 +756,7 @@ public void testLeakingSortValues() throws Exception { + " }\n" + " }\n" + " }\n" - + " }\n", XContentType.JSON)); + + " }}\n")); ensureGreen(); client().prepareIndex("test").setId("1").setSource("{\n" diff --git a/server/src/test/java/org/elasticsearch/search/query/SimpleQueryStringIT.java b/server/src/test/java/org/elasticsearch/search/query/SimpleQueryStringIT.java index bc3f387afc3e3..dde2dc53514a5 100644 --- a/server/src/test/java/org/elasticsearch/search/query/SimpleQueryStringIT.java +++ b/server/src/test/java/org/elasticsearch/search/query/SimpleQueryStringIT.java @@ -336,18 +336,16 @@ public void testLenientFlagBeingTooLenient() throws Exception { public void testSimpleQueryStringAnalyzeWildcard() throws ExecutionException, InterruptedException, IOException { String mapping = Strings.toString(XContentFactory.jsonBuilder() .startObject() - .startObject("type1") .startObject("properties") .startObject("location") .field("type", "text") .field("analyzer", "standard") .endObject() .endObject() - .endObject() .endObject()); CreateIndexRequestBuilder mappingRequest = client().admin().indices().prepareCreate("test1") - .addMapping("type1", mapping, XContentType.JSON); + .setMapping(mapping); mappingRequest.get(); indexRandom(true, client().prepareIndex("test1").setId("1").setSource("location", "Köln")); refresh(); @@ -386,19 +384,17 @@ public void testEmptySimpleQueryStringWithAnalysis() throws Exception { // https://github.com/elastic/elasticsearch/issues/18202 String mapping = Strings.toString(XContentFactory.jsonBuilder() .startObject() - .startObject("type1") .startObject("properties") .startObject("body") .field("type", "text") .field("analyzer", "stop") .endObject() .endObject() - .endObject() .endObject()); CreateIndexRequestBuilder mappingRequest = client().admin().indices() .prepareCreate("test1") - .addMapping("type1", mapping, XContentType.JSON); + .setMapping(mapping); mappingRequest.get(); indexRandom(true, client().prepareIndex("test1").setId("1").setSource("body", "Some Text")); refresh(); diff --git a/server/src/test/java/org/elasticsearch/search/slice/SearchSliceIT.java b/server/src/test/java/org/elasticsearch/search/slice/SearchSliceIT.java index 3d43662fca8c5..9c0310fd45772 100644 --- a/server/src/test/java/org/elasticsearch/search/slice/SearchSliceIT.java +++ b/server/src/test/java/org/elasticsearch/search/slice/SearchSliceIT.java @@ -29,7 +29,6 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.search.Scroll; import org.elasticsearch.search.SearchException; import org.elasticsearch.search.SearchHit; @@ -37,9 +36,9 @@ import org.elasticsearch.test.ESIntegTestCase; import java.io.IOException; -import java.util.List; import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.concurrent.ExecutionException; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; @@ -52,7 +51,6 @@ public class SearchSliceIT extends ESIntegTestCase { private void setupIndex(int numDocs, int numberOfShards) throws IOException, ExecutionException, InterruptedException { String mapping = Strings.toString(XContentFactory.jsonBuilder(). startObject() - .startObject("type") .startObject("properties") .startObject("invalid_random_kw") .field("type", "keyword") @@ -67,11 +65,10 @@ private void setupIndex(int numDocs, int numberOfShards) throws IOException, Exe .field("doc_values", "false") .endObject() .endObject() - .endObject() .endObject()); assertAcked(client().admin().indices().prepareCreate("test") .setSettings(Settings.builder().put("number_of_shards", numberOfShards).put("index.max_slices_per_scroll", 10000)) - .addMapping("type", mapping, XContentType.JSON)); + .setMapping(mapping)); ensureGreen(); List requests = new ArrayList<>(); diff --git a/server/src/test/java/org/elasticsearch/search/sort/SimpleSortIT.java b/server/src/test/java/org/elasticsearch/search/sort/SimpleSortIT.java index faea56f47d326..237235a0a230e 100644 --- a/server/src/test/java/org/elasticsearch/search/sort/SimpleSortIT.java +++ b/server/src/test/java/org/elasticsearch/search/sort/SimpleSortIT.java @@ -26,7 +26,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.script.MockScriptPlugin; @@ -225,7 +224,6 @@ public void testSimpleSorts() throws Exception { public void testSortMinValueScript() throws IOException { String mapping = Strings.toString(jsonBuilder() .startObject() - .startObject("type1") .startObject("properties") .startObject("lvalue") .field("type", "long") @@ -240,10 +238,9 @@ public void testSortMinValueScript() throws IOException { .field("type", "geo_point") .endObject() .endObject() - .endObject() .endObject()); - assertAcked(prepareCreate("test").addMapping("type1", mapping, XContentType.JSON)); + assertAcked(prepareCreate("test").setMapping(mapping)); ensureGreen(); for (int i = 0; i < 10; i++) { @@ -344,7 +341,6 @@ public void testDocumentsWithNullValue() throws Exception { // be propagated to all nodes yet and sort operation fail when the sort field is not defined String mapping = Strings.toString(jsonBuilder() .startObject() - .startObject("type1") .startObject("properties") .startObject("id") .field("type", "keyword") @@ -353,9 +349,8 @@ public void testDocumentsWithNullValue() throws Exception { .field("type", "keyword") .endObject() .endObject() - .endObject() .endObject()); - assertAcked(prepareCreate("test").addMapping("type1", mapping, XContentType.JSON)); + assertAcked(prepareCreate("test").setMapping(mapping)); ensureGreen(); client().prepareIndex("test") diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/license/MachineLearningLicensingTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/license/MachineLearningLicensingTests.java index 760a98a3316f1..f819cff871637 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/license/MachineLearningLicensingTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/license/MachineLearningLicensingTests.java @@ -152,7 +152,7 @@ public void testMachineLearningPutDatafeedActionRestricted() throws Exception { // test that license restricted apis do not work ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> { PlainActionFuture listener = PlainActionFuture.newFuture(); - client().execute(PutDatafeedAction.INSTANCE, + client().execute(PutDatafeedAction.INSTANCE, new PutDatafeedAction.Request(createDatafeed(datafeedId, jobId, Collections.singletonList(jobId))), listener); listener.actionGet(); }); @@ -166,7 +166,7 @@ public void testMachineLearningPutDatafeedActionRestricted() throws Exception { assertMLAllowed(true); // test that license restricted apis do now work PlainActionFuture listener = PlainActionFuture.newFuture(); - client().execute(PutDatafeedAction.INSTANCE, + client().execute(PutDatafeedAction.INSTANCE, new PutDatafeedAction.Request(createDatafeed(datafeedId, jobId, Collections.singletonList(jobId))), listener); PutDatafeedAction.Response response = listener.actionGet(); assertNotNull(response); @@ -177,8 +177,7 @@ public void testAutoCloseJobWithDatafeed() throws Exception { String datafeedId = jobId + "-datafeed"; assertMLAllowed(true); String datafeedIndex = jobId + "-data"; - prepareCreate(datafeedIndex).addMapping("type", "{\"type\":{\"properties\":{\"time\":{\"type\":\"date\"}}}}", - XContentType.JSON).get(); + prepareCreate(datafeedIndex).setMapping("{\"_doc\":{\"properties\":{\"time\":{\"type\":\"date\"}}}}").get(); // put job PlainActionFuture putJobListener = PlainActionFuture.newFuture(); @@ -187,7 +186,7 @@ public void testAutoCloseJobWithDatafeed() throws Exception { assertNotNull(putJobResponse); // put datafeed PlainActionFuture putDatafeedListener = PlainActionFuture.newFuture(); - client().execute(PutDatafeedAction.INSTANCE, + client().execute(PutDatafeedAction.INSTANCE, new PutDatafeedAction.Request(createDatafeed(datafeedId, jobId, Collections.singletonList(datafeedIndex))), putDatafeedListener); PutDatafeedAction.Response putDatafeedResponse = putDatafeedListener.actionGet(); @@ -276,15 +275,14 @@ public void testMachineLearningStartDatafeedActionRestricted() throws Exception String datafeedId = jobId + "-datafeed"; assertMLAllowed(true); String datafeedIndex = jobId + "-data"; - prepareCreate(datafeedIndex).addMapping("type", "{\"type\":{\"properties\":{\"time\":{\"type\":\"date\"}}}}", - XContentType.JSON).get(); + prepareCreate(datafeedIndex).setMapping("{\"_doc\":{\"properties\":{\"time\":{\"type\":\"date\"}}}}").get(); // test that license restricted apis do now work PlainActionFuture putJobListener = PlainActionFuture.newFuture(); client().execute(PutJobAction.INSTANCE, new PutJobAction.Request(createJob(jobId)), putJobListener); PutJobAction.Response putJobResponse = putJobListener.actionGet(); assertNotNull(putJobResponse); PlainActionFuture putDatafeedListener = PlainActionFuture.newFuture(); - client().execute(PutDatafeedAction.INSTANCE, + client().execute(PutDatafeedAction.INSTANCE, new PutDatafeedAction.Request(createDatafeed(datafeedId, jobId, Collections.singletonList(datafeedIndex))), putDatafeedListener); PutDatafeedAction.Response putDatafeedResponse = putDatafeedListener.actionGet(); @@ -340,15 +338,14 @@ public void testMachineLearningStopDatafeedActionNotRestricted() throws Exceptio String datafeedId = jobId + "-datafeed"; assertMLAllowed(true); String datafeedIndex = jobId + "-data"; - prepareCreate(datafeedIndex).addMapping("type", "{\"type\":{\"properties\":{\"time\":{\"type\":\"date\"}}}}", - XContentType.JSON).get(); + prepareCreate(datafeedIndex).setMapping("{\"_doc\":{\"properties\":{\"time\":{\"type\":\"date\"}}}}").get(); // test that license restricted apis do now work PlainActionFuture putJobListener = PlainActionFuture.newFuture(); client().execute(PutJobAction.INSTANCE, new PutJobAction.Request(createJob(jobId)), putJobListener); PutJobAction.Response putJobResponse = putJobListener.actionGet(); assertNotNull(putJobResponse); PlainActionFuture putDatafeedListener = PlainActionFuture.newFuture(); - client().execute(PutDatafeedAction.INSTANCE, + client().execute(PutDatafeedAction.INSTANCE, new PutDatafeedAction.Request(createDatafeed(datafeedId, jobId, Collections.singletonList(datafeedIndex))), putDatafeedListener); PutDatafeedAction.Response putDatafeedResponse = putDatafeedListener.actionGet(); @@ -358,7 +355,7 @@ public void testMachineLearningStopDatafeedActionNotRestricted() throws Exceptio AcknowledgedResponse openJobResponse = openJobListener.actionGet(); assertNotNull(openJobResponse); PlainActionFuture startDatafeedListener = PlainActionFuture.newFuture(); - client().execute(StartDatafeedAction.INSTANCE, + client().execute(StartDatafeedAction.INSTANCE, new StartDatafeedAction.Request(datafeedId, 0L), startDatafeedListener); AcknowledgedResponse startDatafeedResponse = startDatafeedListener.actionGet(); assertNotNull(startDatafeedResponse); @@ -457,7 +454,7 @@ public void testMachineLearningDeleteDatafeedActionNotRestricted() throws Except PutJobAction.Response putJobResponse = putJobListener.actionGet(); assertNotNull(putJobResponse); PlainActionFuture putDatafeedListener = PlainActionFuture.newFuture(); - client().execute(PutDatafeedAction.INSTANCE, + client().execute(PutDatafeedAction.INSTANCE, new PutDatafeedAction.Request(createDatafeed(datafeedId, jobId, Collections.singletonList(jobId))), putDatafeedListener); PutDatafeedAction.Response putDatafeedResponse = putDatafeedListener.actionGet(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java index 368cef6336966..4d7b22af1cd32 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java @@ -360,7 +360,7 @@ public void prepareIndexIfNeededThenExecute(final Consumer consumer, final Tuple mappingAndSettings = parseMappingAndSettingsFromTemplateBytes(mappingSource); CreateIndexRequest request = new CreateIndexRequest(indexState.concreteIndexName) .alias(new Alias(this.aliasName)) - .mapping(MapperService.SINGLE_MAPPING_NAME, mappingAndSettings.v1(), XContentType.JSON) + .mapping("{\"_doc\":" + mappingAndSettings.v1() + "}") .waitForActiveShards(ActiveShardCount.ALL) .settings(mappingAndSettings.v2()); executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, request, diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/throttler/ActionThrottleTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/throttler/ActionThrottleTests.java index 86cd201a9330f..bb45e16ed7263 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/throttler/ActionThrottleTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/throttler/ActionThrottleTests.java @@ -284,16 +284,14 @@ public void testFailingActionDoesGetThrottled() throws Exception { // create a mapping with a wrong @timestamp field, so that the index action of the watch below will fail String mapping = Strings.toString(XContentFactory.jsonBuilder() .startObject() - .startObject("bar") .startObject("properties") .startObject("@timestamp") .field("type", "integer") .endObject() .endObject() - .endObject() .endObject()); - client().admin().indices().prepareCreate("foo").addMapping("bar", mapping, XContentType.JSON).get(); + client().admin().indices().prepareCreate("foo").setMapping(mapping).get(); TimeValue throttlePeriod = new TimeValue(60, TimeUnit.MINUTES); From d971d0ab356ab877d2d35f60b07a8ce3cc3c6d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Thu, 2 Jan 2020 14:28:50 +0100 Subject: [PATCH 350/686] Fix type conversion problem in Eclipse (#50549) Eclipse 4.13 shows a type mismatch error in the affected line because it cannot correctly infer the boolean return type for the method call. Assigning return value to a local variable resolves this problem. --- .../elasticsearch/xpack/ml/integration/ClassificationIT.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java index 978b11efb0f39..6d96f627b3f00 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.ml.integration; import com.google.common.collect.Ordering; + import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.admin.indices.get.GetIndexAction; import org.elasticsearch.action.admin.indices.get.GetIndexRequest; @@ -192,7 +193,9 @@ public void testWithOnlyTrainingRowsAndTrainingPercentIsFifty(String jobId, assertTopClasses(resultsObject, numTopClasses, dependentVariable, dependentVariableValues); // Let's just assert there's both training and non-training results - if (getFieldValue(resultsObject, "is_training")) { + // + boolean isTraining = getFieldValue(resultsObject, "is_training"); + if (isTraining) { trainingRowsCount++; } else { nonTrainingRowsCount++; From 5f981c19f07bd1b6446de6484638603eccfea16f Mon Sep 17 00:00:00 2001 From: Oleg <25862932+olegbonar@users.noreply.github.com> Date: Thu, 2 Jan 2020 18:22:40 +0400 Subject: [PATCH 351/686] Deprecate the 'local' parameter of /_cat/nodes (#50499) The cat nodes API performs a `ClusterStateAction` then a `NodesInfoAction`. Today it accepts the `?local` parameter and passes this to the `ClusterStateAction` but this parameter has no effect on the `NodesInfoAction`. This is surprising, because `GET _cat/nodes?local` looks like it might be a completely local call but in fact it still depends on every node in the cluster. This commit deprecates the `?local` parameter on this API so that it can be removed in 8.0. Relates #50088 --- docs/reference/cat/nodes.asciidoc | 10 ++++++++++ .../resources/rest-api-spec/api/cat.nodes.json | 6 +++++- .../rest/action/cat/RestNodesAction.java | 9 +++++++++ .../rest/action/cat/RestNodesActionTests.java | 15 +++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/docs/reference/cat/nodes.asciidoc b/docs/reference/cat/nodes.asciidoc index 1c77f17d7e4f6..506bc6f5c2721 100644 --- a/docs/reference/cat/nodes.asciidoc +++ b/docs/reference/cat/nodes.asciidoc @@ -286,6 +286,16 @@ Number of suggest operations, such as `0`. include::{docdir}/rest-api/common-parms.asciidoc[tag=help] include::{docdir}/rest-api/common-parms.asciidoc[tag=local] ++ +-- +`local`:: +(Optional, boolean) If `true`, the request computes the list of selected nodes +from the local cluster state. Defaults to `false`, which means the list of +selected nodes is computed from the cluster state on the master node. In either +case the coordinating node sends a request for further information to each +selected node. deprecated::[8.0,This parameter does not cause this API to act +locally. It will be removed in version 8.0.] +-- include::{docdir}/rest-api/common-parms.asciidoc[tag=master-timeout] diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json index 7fd96f8b387c3..753a6aab1d5f3 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json @@ -43,7 +43,11 @@ }, "local":{ "type":"boolean", - "description":"Return local information, do not retrieve the state from master node (default: false)" + "description":"Calculate the selected nodes using the local cluster state rather than the state from master node (default: false)", + "deprecated":{ + "version":"8.0.0", + "description":"This parameter does not cause this API to act locally." + } }, "master_timeout":{ "type":"time", diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/RestNodesAction.java b/server/src/main/java/org/elasticsearch/rest/action/cat/RestNodesAction.java index aa780f104c27d..849fbdac49224 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/RestNodesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/RestNodesAction.java @@ -19,6 +19,7 @@ package org.elasticsearch.rest.action.cat; +import org.apache.logging.log4j.LogManager; import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; @@ -33,6 +34,7 @@ import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.Strings; import org.elasticsearch.common.Table; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.ByteSizeValue; @@ -67,6 +69,10 @@ import static org.elasticsearch.rest.RestRequest.Method.GET; public class RestNodesAction extends AbstractCatAction { + private static final DeprecationLogger deprecationLogger = new DeprecationLogger( + LogManager.getLogger(RestNodesAction.class)); + static final String LOCAL_DEPRECATED_MESSAGE = "Deprecated parameter [local] used. This parameter does not cause this API to act " + + "locally, and should not be used. It will be unsupported in version 8.0."; public RestNodesAction(RestController controller) { controller.registerHandler(GET, "/_cat/nodes", this); @@ -86,6 +92,9 @@ protected void documentation(StringBuilder sb) { public RestChannelConsumer doCatRequest(final RestRequest request, final NodeClient client) { final ClusterStateRequest clusterStateRequest = new ClusterStateRequest(); clusterStateRequest.clear().nodes(true); + if (request.hasParam("local")) { + deprecationLogger.deprecated(LOCAL_DEPRECATED_MESSAGE); + } clusterStateRequest.local(request.paramAsBoolean("local", clusterStateRequest.local())); clusterStateRequest.masterNodeTimeout(request.paramAsTime("master_timeout", clusterStateRequest.masterNodeTimeout())); final boolean fullId = request.paramAsBoolean("full_id", false); diff --git a/server/src/test/java/org/elasticsearch/rest/action/cat/RestNodesActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/cat/RestNodesActionTests.java index bbdb098a42d34..05b14d6b1cfc4 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/cat/RestNodesActionTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/cat/RestNodesActionTests.java @@ -23,13 +23,16 @@ import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; +import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestController; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.rest.FakeRestRequest; +import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.usage.UsageService; import org.junit.Before; @@ -65,4 +68,16 @@ public void testBuildTableDoesNotThrowGivenNullNodeInfoAndStats() { action.buildTable(false, new FakeRestRequest(), clusterStateResponse, nodesInfoResponse, nodesStatsResponse); } + + public void testCatNodesWithLocalDeprecationWarning() { + TestThreadPool threadPool = new TestThreadPool(RestNodesActionTests.class.getName()); + NodeClient client = new NodeClient(Settings.EMPTY, threadPool); + FakeRestRequest request = new FakeRestRequest(); + request.params().put("local", randomFrom("", "true", "false")); + + action.doCatRequest(request, client); + assertWarnings(RestNodesAction.LOCAL_DEPRECATED_MESSAGE); + + terminate(threadPool); + } } From aa4dd23485582d9e4d1b895022b8b2c378be0edd Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Thu, 2 Jan 2020 09:47:12 -0500 Subject: [PATCH 352/686] Make some ObjectParsers final (#50471) We have about 800 `ObjectParsers` in Elasticsearch, about 700 of which are final. This is *probably* the right way to declare them because in practice we never mutate them after they are built. And we certainly don't change the static reference. Anyway, this adds `final` to a bunch of these parsers, mostly the ones in xpack and their "paired" parsers in the high level rest client. I picked these just to have somewhere to break the up the change so it wouldn't be huge. I found the non-final parsers with this: ``` diff \ <(find . -type f -name '*.java' -exec grep -iHe 'static.*PARSER\s*=' {} \+ | sort) \ <(find . -type f -name '*.java' -exec grep -iHe 'static.*final.*PARSER\s*=' {} \+ | sort) \ 2>&1 | grep '^<' ``` --- .../org/elasticsearch/client/core/MainResponse.java | 3 +-- .../org/elasticsearch/client/ilm/LifecyclePolicy.java | 2 +- .../client/ml/dataframe/explain/FieldSelection.java | 2 +- .../client/security/CreateApiKeyResponse.java | 2 +- .../client/security/GetApiKeyResponse.java | 7 ++++--- .../client/security/InvalidateApiKeyResponse.java | 10 ++++++---- .../elasticsearch/client/security/support/ApiKey.java | 2 +- .../aggregations/matrix/stats/ParsedMatrixStats.java | 2 +- .../xpack/ccr/action/ShardFollowTask.java | 2 +- .../elasticsearch/xpack/core/ilm/LifecyclePolicy.java | 2 +- .../xpack/core/ml/action/CloseJobAction.java | 2 +- .../xpack/core/ml/action/IsolateDatafeedAction.java | 3 +-- .../xpack/core/ml/action/OpenJobAction.java | 2 +- .../core/ml/action/RevertModelSnapshotAction.java | 3 +-- .../core/ml/action/StartDataFrameAnalyticsAction.java | 2 +- .../xpack/core/ml/action/StartDatafeedAction.java | 6 +++++- .../xpack/core/ml/action/StopDatafeedAction.java | 3 +-- .../core/ml/dataframe/explain/FieldSelection.java | 3 ++- .../xpack/core/security/action/ApiKey.java | 2 +- .../core/security/action/CreateApiKeyResponse.java | 2 +- .../xpack/core/security/action/GetApiKeyResponse.java | 2 +- .../security/action/InvalidateApiKeyResponse.java | 11 +++++++---- .../core/transform/action/PreviewTransformAction.java | 2 +- 23 files changed, 42 insertions(+), 35 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainResponse.java index 96810fe8c8b62..09fc2b49a0156 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainResponse.java @@ -27,8 +27,7 @@ public class MainResponse { - @SuppressWarnings("unchecked") - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(MainResponse.class.getName(), true, args -> { return new MainResponse((String) args[0], (Version) args[1], (String) args[2], (String) args[3], (String) args[4]); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ilm/LifecyclePolicy.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ilm/LifecyclePolicy.java index e3ead01006256..b6d16ddccfe89 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ilm/LifecyclePolicy.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ilm/LifecyclePolicy.java @@ -44,7 +44,7 @@ public class LifecyclePolicy implements ToXContentObject { static final ParseField PHASES_FIELD = new ParseField("phases"); @SuppressWarnings("unchecked") - public static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("lifecycle_policy", true, + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("lifecycle_policy", true, (a, name) -> { List phases = (List) a[0]; Map phaseMap = phases.stream().collect(Collectors.toMap(Phase::getName, Function.identity())); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/explain/FieldSelection.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/explain/FieldSelection.java index 4483b6fa5e09a..997fa8727692a 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/explain/FieldSelection.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/explain/FieldSelection.java @@ -57,7 +57,7 @@ public String toString() { } @SuppressWarnings("unchecked") - public static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("field_selection", true, + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("field_selection", true, a -> new FieldSelection((String) a[0], new HashSet<>((List) a[1]), (boolean) a[2], (boolean) a[3], (FeatureType) a[4], (String) a[5])); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyResponse.java index 9c5037237407b..0e99f0476bab0 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyResponse.java @@ -89,7 +89,7 @@ public boolean equals(Object obj) { && Objects.equals(expiration, other.expiration); } - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("create_api_key_response", + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("create_api_key_response", args -> new CreateApiKeyResponse((String) args[0], (String) args[1], new SecureString((String) args[2]), (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]))); static { diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyResponse.java index 58e3e8effbb09..306866bad6955 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyResponse.java @@ -73,9 +73,10 @@ public boolean equals(Object obj) { } @SuppressWarnings("unchecked") - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("get_api_key_response", args -> { - return (args[0] == null) ? GetApiKeyResponse.emptyResponse() : new GetApiKeyResponse((List) args[0]); - }); + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "get_api_key_response", + args -> { return (args[0] == null) ? GetApiKeyResponse.emptyResponse() : new GetApiKeyResponse((List) args[0]); } + ); static { PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ApiKey.fromXContent(p), new ParseField("api_keys")); } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyResponse.java index 48df9d0f7f12b..14deac350506d 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyResponse.java @@ -74,10 +74,12 @@ public List getErrors() { } @SuppressWarnings("unchecked") - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("invalidate_api_key_response", - args -> { - return new InvalidateApiKeyResponse((List) args[0], (List) args[1], (List) args[3]); - }); + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "invalidate_api_key_response", + args -> { + return new InvalidateApiKeyResponse((List) args[0], (List) args[1], (List) args[3]); + } + ); static { PARSER.declareStringArray(constructorArg(), new ParseField("invalidated_api_keys")); PARSER.declareStringArray(constructorArg(), new ParseField("previously_invalidated_api_keys")); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java index d7065a311a53e..747010fd09f98 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java @@ -127,7 +127,7 @@ public boolean equals(Object obj) { && Objects.equals(realm, other.realm); } - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("api_key", args -> { + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("api_key", args -> { return new ApiKey((String) args[0], (String) args[1], Instant.ofEpochMilli((Long) args[2]), (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6]); }); diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java index a0b9224ad0d25..ba28e0b0ee361 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java +++ b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java @@ -151,7 +151,7 @@ private static T checkedGet(final Map values, final String fieldN return values.get(fieldName); } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedMatrixStats.class.getSimpleName(), true, ParsedMatrixStats::new); static { declareAggregationFields(PARSER); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java index 28f06505a0060..bcadfe4f80717 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java @@ -44,7 +44,7 @@ public class ShardFollowTask extends ImmutableFollowParameters implements Persis private static final ParseField HEADERS = new ParseField("headers"); @SuppressWarnings("unchecked") - private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, (a) -> new ShardFollowTask((String) a[0], new ShardId((String) a[1], (String) a[2], (int) a[3]), new ShardId((String) a[4], (String) a[5], (int) a[6]), (Integer) a[7], (Integer) a[8], (Integer) a[9], (Integer) a[10], (ByteSizeValue) a[11], (ByteSizeValue) a[12], diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicy.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicy.java index 7c810d12552bb..aa16473f2111f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicy.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicy.java @@ -47,7 +47,7 @@ public class LifecyclePolicy extends AbstractDiffable public static final ParseField PHASES_FIELD = new ParseField("phases"); @SuppressWarnings("unchecked") - public static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("lifecycle_policy", false, + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("lifecycle_policy", false, (a, name) -> { List phases = (List) a[0]; Map phaseMap = phases.stream().collect(Collectors.toMap(Phase::getName, Function.identity())); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java index 4e19af64758ac..97debd2bc55e9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java @@ -40,7 +40,7 @@ public static class Request extends BaseTasksRequest implements ToXCont public static final ParseField TIMEOUT = new ParseField("timeout"); public static final ParseField FORCE = new ParseField("force"); public static final ParseField ALLOW_NO_JOBS = new ParseField("allow_no_jobs"); - public static ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); + public static final ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); static { PARSER.declareString(Request::setJobId, Job.ID); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/IsolateDatafeedAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/IsolateDatafeedAction.java index a3d8088d138ab..4187b347a8da6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/IsolateDatafeedAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/IsolateDatafeedAction.java @@ -46,8 +46,7 @@ private IsolateDatafeedAction() { public static class Request extends BaseTasksRequest implements ToXContentObject { - public static ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); - + public static final ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); static { PARSER.declareString((request, datafeedId) -> request.datafeedId = datafeedId, DatafeedConfig.ID); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/OpenJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/OpenJobAction.java index 63b664c0bd99e..67d5d96972ac3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/OpenJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/OpenJobAction.java @@ -124,7 +124,7 @@ public static class JobParams implements PersistentTaskParams { public static final ParseField TIMEOUT = new ParseField("timeout"); public static final ParseField JOB = new ParseField("job"); - public static ObjectParser PARSER = new ObjectParser<>(MlTasks.JOB_TASK_NAME, true, JobParams::new); + public static final ObjectParser PARSER = new ObjectParser<>(MlTasks.JOB_TASK_NAME, true, JobParams::new); static { PARSER.declareString(JobParams::setJobId, Job.ID); PARSER.declareString((params, val) -> diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/RevertModelSnapshotAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/RevertModelSnapshotAction.java index aeabe2102f09d..dfeb94d73291c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/RevertModelSnapshotAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/RevertModelSnapshotAction.java @@ -42,8 +42,7 @@ public static class Request extends AcknowledgedRequest implements ToXC public static final ParseField SNAPSHOT_ID = new ParseField("snapshot_id"); public static final ParseField DELETE_INTERVENING = new ParseField("delete_intervening_results"); - private static ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); - + private static final ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); static { PARSER.declareString((request, jobId) -> request.jobId = jobId, Job.ID); PARSER.declareString((request, snapshotId) -> request.snapshotId = snapshotId, SNAPSHOT_ID); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartDataFrameAnalyticsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartDataFrameAnalyticsAction.java index c9e486fb86cc0..0455e1eb1b35d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartDataFrameAnalyticsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartDataFrameAnalyticsAction.java @@ -157,7 +157,7 @@ public static class TaskParams implements PersistentTaskParams { private static final ParseField PROGRESS_ON_START = new ParseField("progress_on_start"); @SuppressWarnings("unchecked") - public static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( MlTasks.DATA_FRAME_ANALYTICS_TASK_NAME, true, a -> new TaskParams((String) a[0], (String) a[1], (List) a[2], (Boolean) a[3])); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartDatafeedAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartDatafeedAction.java index 2e2f837039c51..afd58e0b70554 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartDatafeedAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartDatafeedAction.java @@ -136,7 +136,11 @@ public static class DatafeedParams implements PersistentTaskParams { public static final ParseField INDICES = new ParseField("indices"); - public static ObjectParser PARSER = new ObjectParser<>(MlTasks.DATAFEED_TASK_NAME, true, DatafeedParams::new); + public static final ObjectParser PARSER = new ObjectParser<>( + MlTasks.DATAFEED_TASK_NAME, + true, + DatafeedParams::new + ); static { PARSER.declareString((params, datafeedId) -> params.datafeedId = datafeedId, DatafeedConfig.ID); PARSER.declareString((params, startTime) -> params.startTime = parseDateOrThrow( diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java index 1c21fde4382df..96d1fdb31d479 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java @@ -44,8 +44,7 @@ public static class Request extends BaseTasksRequest implements ToXCont public static final ParseField FORCE = new ParseField("force"); public static final ParseField ALLOW_NO_DATAFEEDS = new ParseField("allow_no_datafeeds"); - public static ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); - + public static final ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); static { PARSER.declareString((request, datafeedId) -> request.datafeedId = datafeedId, DatafeedConfig.ID); PARSER.declareString((request, val) -> diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/explain/FieldSelection.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/explain/FieldSelection.java index 57fae51d36643..609cec3734562 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/explain/FieldSelection.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/explain/FieldSelection.java @@ -46,7 +46,8 @@ public String toString() { } } - public static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("field_selection", + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("field_selection", a -> new FieldSelection((String) a[0], new HashSet<>((List) a[1]), (boolean) a[2], (boolean) a[3], (FeatureType) a[4], (String) a[5])); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java index 214881f34ea01..9c6d6a9f9b63a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java @@ -147,7 +147,7 @@ public boolean equals(Object obj) { && Objects.equals(realm, other.realm); } - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("api_key", args -> { + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("api_key", args -> { return new ApiKey((String) args[0], (String) args[1], Instant.ofEpochMilli((Long) args[2]), (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6]); }); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java index 56c855fe26b00..2fe2c36f9ceac 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java @@ -31,7 +31,7 @@ */ public final class CreateApiKeyResponse extends ActionResponse implements ToXContentObject { - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("create_api_key_response", + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("create_api_key_response", args -> new CreateApiKeyResponse((String) args[0], (String) args[1], new SecureString((String) args[2]), (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]))); static { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java index f4765721819b5..19c826da524da 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java @@ -63,7 +63,7 @@ public void writeTo(StreamOutput out) throws IOException { } @SuppressWarnings("unchecked") - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("get_api_key_response", args -> { + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("get_api_key_response", args -> { return (args[0] == null) ? GetApiKeyResponse.emptyResponse() : new GetApiKeyResponse((List) args[0]); }); static { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java index 39850c0fff88d..d6b03d6e5fdbd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java @@ -109,10 +109,13 @@ public void writeTo(StreamOutput out) throws IOException { out.writeCollection(errors, StreamOutput::writeException); } - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("invalidate_api_key_response", - args -> { - return new InvalidateApiKeyResponse((List) args[0], (List) args[1], (List) args[3]); - }); + @SuppressWarnings("unchecked") + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "invalidate_api_key_response", + args -> { + return new InvalidateApiKeyResponse((List) args[0], (List) args[1], (List) args[3]); + } + ); static { PARSER.declareStringArray(constructorArg(), new ParseField("invalidated_api_keys")); PARSER.declareStringArray(constructorArg(), new ParseField("previously_invalidated_api_keys")); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java index 9078d5952e2af..66c57e2bf0080 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java @@ -145,7 +145,7 @@ public static class Response extends ActionResponse implements ToXContentObject public static ParseField PREVIEW = new ParseField("preview"); public static ParseField MAPPINGS = new ParseField("mappings"); - static ObjectParser PARSER = new ObjectParser<>("data_frame_transform_preview", Response::new); + static final ObjectParser PARSER = new ObjectParser<>("data_frame_transform_preview", Response::new); static { PARSER.declareObjectArray(Response::setDocs, (p, c) -> p.mapOrdered(), PREVIEW); PARSER.declareObject(Response::setMappings, (p, c) -> p.mapOrdered(), MAPPINGS); From 6b789f2eec18906bf4c86d6aa4c1aaa7432f20e1 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Thu, 2 Jan 2020 10:02:55 -0500 Subject: [PATCH 353/686] Docs: Refine note about `after_key` (#50475) * Docs: Refine note about `after_key` I was curious about composite aggregations, specifically I wanted to know how to write a composite aggregation that had all of its buckets filtered out so you *had* to use the `after_key`. Then I saw that we've declared composite aggregations not to work with pipelines in #44180. So I'm not sure you *can* do that any more. Which makes the note about `after_key` inaccurate. This rejiggers that section of the docs a little so it is more obvious that you send the `after_key` back to us. And so it is more obvious that you should *only* use the `after_key` that we give you rather than try to work it out for yourself. * Apply suggestions from code review Co-Authored-By: James Rodewig Co-authored-by: James Rodewig --- .../bucket/composite-aggregation.asciidoc | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/reference/aggregations/bucket/composite-aggregation.asciidoc b/docs/reference/aggregations/bucket/composite-aggregation.asciidoc index 226d9c2d7ade1..ec253a5556059 100644 --- a/docs/reference/aggregations/bucket/composite-aggregation.asciidoc +++ b/docs/reference/aggregations/bucket/composite-aggregation.asciidoc @@ -441,7 +441,7 @@ GET /_search ... "aggregations": { "my_buckets": { - "after_key": { <1> + "after_key": { "date": 1494288000000, "product": "mad max" }, @@ -467,17 +467,9 @@ GET /_search -------------------------------------------------- // TESTRESPONSE[s/\.\.\.//] -<1> The last composite bucket returned by the query. - -NOTE: The `after_key` is equals to the last bucket returned in the response before -any filtering that could be done by <>. -If all buckets are filtered/removed by a pipeline aggregation, the `after_key` will contain -the last bucket before filtering. - -The `after` parameter can be used to retrieve the composite buckets that are **after** -the last composite buckets returned in a previous round. -For the example below the last bucket can be found in `after_key` and the next -round of result can be retrieved with: +To get the next set of buckets, resend the same aggregation with the `after` +parameter set to the `after_key` value returned in the response. +For example, this request uses the `after_key` value provided in the previous response: [source,console,id=composite-aggregation-after-example] -------------------------------------------------- @@ -501,6 +493,10 @@ GET /_search <1> Should restrict the aggregation to buckets that sort **after** the provided values. +NOTE: The `after_key` is *usually* the key to the last bucket returned in +the response, but that isn't guaranteed. Always use the returned `after_key` instead +of derriving it from the buckets. + ==== Early termination For optimal performance the <> should be set on the index so that it matches From 188677118b1dfd8240f10a1305c69b1fbc493734 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 2 Jan 2020 16:30:23 +0100 Subject: [PATCH 354/686] Make EC2 Discovery Plugin Retry Requests (#50550) Use the default retry condition instead of never retrying in the discovery plugin causing hot retries upstream and add a test that verifies retrying works. Closes #50462 --- .../discovery/ec2/AwsEc2ServiceImpl.java | 17 +- .../discovery/ec2/EC2RetriesTests.java | 266 ++++++++++++++++++ 2 files changed, 268 insertions(+), 15 deletions(-) create mode 100644 plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/EC2RetriesTests.java diff --git a/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImpl.java b/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImpl.java index 739b964925c3e..546634c88cf45 100644 --- a/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImpl.java +++ b/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImpl.java @@ -25,21 +25,18 @@ import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.http.IdleConnectionReaper; -import com.amazonaws.retry.RetryPolicy; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.AmazonEC2Client; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.common.Randomness; import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.LazyInitializable; -import java.util.Random; import java.util.concurrent.atomic.AtomicReference; class AwsEc2ServiceImpl implements AwsEc2Service { - + private static final Logger logger = LogManager.getLogger(AwsEc2ServiceImpl.class); private final AtomicReference> lazyClientReference = @@ -77,17 +74,7 @@ static ClientConfiguration buildConfiguration(Logger logger, Ec2ClientSettings c clientConfiguration.setProxyPassword(clientSettings.proxyPassword); } // Increase the number of retries in case of 5xx API responses - final Random rand = Randomness.get(); - final RetryPolicy retryPolicy = new RetryPolicy( - RetryPolicy.RetryCondition.NO_RETRY_CONDITION, - (originalRequest, exception, retriesAttempted) -> { - // with 10 retries the max delay time is 320s/320000ms (10 * 2^5 * 1 * 1000) - logger.warn("EC2 API request failed, retry again. Reason was:", exception); - return 1000L * (long) (10d * Math.pow(2, retriesAttempted / 2.0d) * (1.0d + rand.nextDouble())); - }, - 10, - false); - clientConfiguration.setRetryPolicy(retryPolicy); + clientConfiguration.setMaxErrorRetry(10); clientConfiguration.setSocketTimeout(clientSettings.readTimeoutMillis); return clientConfiguration; } diff --git a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/EC2RetriesTests.java b/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/EC2RetriesTests.java new file mode 100644 index 0000000000000..df8effd9548b6 --- /dev/null +++ b/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/EC2RetriesTests.java @@ -0,0 +1,266 @@ +/* + * 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. + */ + +package org.elasticsearch.discovery.ec2; + +import com.amazonaws.http.HttpMethodName; +import com.sun.net.httpserver.HttpServer; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.elasticsearch.Version; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.network.InetAddresses; +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.discovery.SeedHostsProvider; +import org.elasticsearch.discovery.SeedHostsResolver; +import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; +import org.elasticsearch.mocksocket.MockHttpServer; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.transport.nio.MockNioTransport; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; + +import javax.xml.XMLConstants; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamWriter; + +import java.io.IOException; +import java.io.StringWriter; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.is; + +@SuppressForbidden(reason = "use a http server") +public class EC2RetriesTests extends ESTestCase { + + private HttpServer httpServer; + + private ThreadPool threadPool; + + private MockTransportService transportService; + + private NetworkService networkService = new NetworkService(Collections.emptyList()); + + @Before + public void setUp() throws Exception { + httpServer = MockHttpServer.createHttp(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); + httpServer.start(); + threadPool = new TestThreadPool(EC2RetriesTests.class.getName()); + final MockNioTransport transport = new MockNioTransport(Settings.EMPTY, Version.CURRENT, threadPool, networkService, + PageCacheRecycler.NON_RECYCLING_INSTANCE, new NamedWriteableRegistry(Collections.emptyList()), + new NoneCircuitBreakerService()); + transportService = + new MockTransportService(Settings.EMPTY, transport, threadPool, TransportService.NOOP_TRANSPORT_INTERCEPTOR, null); + super.setUp(); + } + + @After + public void tearDown() throws Exception { + try { + IOUtils.close(transportService, () -> terminate(threadPool), () -> httpServer.stop(0)); + } finally { + super.tearDown(); + } + } + + public void testEC2DiscoveryRetriesOnRateLimiting() throws IOException { + final String accessKey = "ec2_access"; + final List hosts = List.of("127.0.0.1:9000"); + final Map failedRequests = new ConcurrentHashMap<>(); + // retry the same request 5 times at most + final int maxRetries = randomIntBetween(1, 5); + httpServer.createContext("/", exchange -> { + if (exchange.getRequestMethod().equals(HttpMethodName.POST.name())) { + final String request = new String(exchange.getRequestBody().readAllBytes(), UTF_8); + final String userAgent = exchange.getRequestHeaders().getFirst("User-Agent"); + if (userAgent != null && userAgent.startsWith("aws-sdk-java")) { + final String auth = exchange.getRequestHeaders().getFirst("Authorization"); + if (auth == null || auth.contains(accessKey) == false) { + throw new IllegalArgumentException("wrong access key: " + auth); + } + if (failedRequests.compute(exchange.getRequestHeaders().getFirst("Amz-sdk-invocation-id"), + (requestId, count) -> Objects.requireNonNullElse(count, 0) + 1) < maxRetries) { + exchange.sendResponseHeaders(HttpStatus.SC_SERVICE_UNAVAILABLE, -1); + return; + } + // Simulate an EC2 DescribeInstancesResponse + byte[] responseBody = null; + for (NameValuePair parse : URLEncodedUtils.parse(request, UTF_8)) { + if ("Action".equals(parse.getName())) { + responseBody = generateDescribeInstancesResponse(hosts); + break; + } + } + responseBody = responseBody == null ? new byte[0] : responseBody; + exchange.getResponseHeaders().set("Content-Type", "text/xml; charset=UTF-8"); + exchange.sendResponseHeaders(HttpStatus.SC_OK, responseBody.length); + exchange.getResponseBody().write(responseBody); + return; + } + } + fail("did not send response"); + }); + + final InetSocketAddress address = httpServer.getAddress(); + final String endpoint = "http://" + InetAddresses.toUriString(address.getAddress()) + ":" + address.getPort(); + final MockSecureSettings mockSecure = new MockSecureSettings(); + mockSecure.setString(Ec2ClientSettings.ACCESS_KEY_SETTING.getKey(), accessKey); + mockSecure.setString(Ec2ClientSettings.SECRET_KEY_SETTING.getKey(), "ec2_secret"); + try (Ec2DiscoveryPlugin plugin = new Ec2DiscoveryPlugin( + Settings.builder().put(Ec2ClientSettings.ENDPOINT_SETTING.getKey(), endpoint).setSecureSettings(mockSecure).build())) { + final SeedHostsProvider seedHostsProvider = plugin.getSeedHostProviders(transportService, networkService).get("ec2").get(); + final SeedHostsResolver resolver = new SeedHostsResolver("test", Settings.EMPTY, transportService, seedHostsProvider); + resolver.start(); + final List addressList = seedHostsProvider.getSeedAddresses(resolver); + assertThat(addressList, Matchers.hasSize(1)); + assertThat(addressList.get(0).toString(), is(hosts.get(0))); + assertThat(failedRequests, aMapWithSize(1)); + assertThat(failedRequests.values().iterator().next(), is(maxRetries)); + } + } + + /** + * Generates a XML response that describe the EC2 instances + * TODO: org.elasticsearch.discovery.ec2.AmazonEC2Fixture uses pretty much the same code. We should dry up that test fixture. + */ + private byte[] generateDescribeInstancesResponse(List nodes) { + final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); + xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); + + final StringWriter out = new StringWriter(); + XMLStreamWriter sw; + try { + sw = xmlOutputFactory.createXMLStreamWriter(out); + sw.writeStartDocument(); + + String namespace = "http://ec2.amazonaws.com/doc/2013-02-01/"; + sw.setDefaultNamespace(namespace); + sw.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, "DescribeInstancesResponse", namespace); + { + sw.writeStartElement("requestId"); + sw.writeCharacters(UUID.randomUUID().toString()); + sw.writeEndElement(); + + sw.writeStartElement("reservationSet"); + { + for (String address : nodes) { + sw.writeStartElement("item"); + { + sw.writeStartElement("reservationId"); + sw.writeCharacters(UUID.randomUUID().toString()); + sw.writeEndElement(); + + sw.writeStartElement("instancesSet"); + { + sw.writeStartElement("item"); + { + sw.writeStartElement("instanceId"); + sw.writeCharacters(UUID.randomUUID().toString()); + sw.writeEndElement(); + + sw.writeStartElement("imageId"); + sw.writeCharacters(UUID.randomUUID().toString()); + sw.writeEndElement(); + + sw.writeStartElement("instanceState"); + { + sw.writeStartElement("code"); + sw.writeCharacters("16"); + sw.writeEndElement(); + + sw.writeStartElement("name"); + sw.writeCharacters("running"); + sw.writeEndElement(); + } + sw.writeEndElement(); + + sw.writeStartElement("privateDnsName"); + sw.writeCharacters(address); + sw.writeEndElement(); + + sw.writeStartElement("dnsName"); + sw.writeCharacters(address); + sw.writeEndElement(); + + sw.writeStartElement("instanceType"); + sw.writeCharacters("m1.medium"); + sw.writeEndElement(); + + sw.writeStartElement("placement"); + { + sw.writeStartElement("availabilityZone"); + sw.writeCharacters("use-east-1e"); + sw.writeEndElement(); + + sw.writeEmptyElement("groupName"); + + sw.writeStartElement("tenancy"); + sw.writeCharacters("default"); + sw.writeEndElement(); + } + sw.writeEndElement(); + + sw.writeStartElement("privateIpAddress"); + sw.writeCharacters(address); + sw.writeEndElement(); + + sw.writeStartElement("ipAddress"); + sw.writeCharacters(address); + sw.writeEndElement(); + } + sw.writeEndElement(); + } + sw.writeEndElement(); + } + sw.writeEndElement(); + } + sw.writeEndElement(); + } + sw.writeEndElement(); + + sw.writeEndDocument(); + sw.flush(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return out.toString().getBytes(UTF_8); + } +} From 4aec2a9de03ec7ab94886c6b8e5d34b1d304432a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Thu, 2 Jan 2020 16:53:56 +0100 Subject: [PATCH 355/686] Delete removed token filter names from SynonymsAnalysisTests (#50438) The `testPreconfiguredTokenFilters` test refers to the `nGram` and `edgeNGram` token filter which are no longer part of the preconfigured token filters, so they can be removed here as well. --- .../common/SynonymsAnalysisTests.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java index 47fb4afae2ca8..992cd11bedf0c 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java @@ -250,8 +250,8 @@ public void testTokenFiltersBypassSynonymAnalysis() throws IOException { public void testPreconfiguredTokenFilters() throws IOException { Set disallowedFilters = new HashSet<>(Arrays.asList( - "common_grams", "edge_ngram", "edgeNGram", "keyword_repeat", "ngram", "nGram", - "shingle", "word_delimiter", "word_delimiter_graph" + "common_grams", "edge_ngram", "keyword_repeat", "ngram", "shingle", + "word_delimiter", "word_delimiter_graph" )); Settings settings = Settings.builder() @@ -260,23 +260,23 @@ public void testPreconfiguredTokenFilters() throws IOException { .put("path.home", createTempDir().toString()) .build(); IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); - - CommonAnalysisPlugin plugin = new CommonAnalysisPlugin(); - - for (PreConfiguredTokenFilter tf : plugin.getPreConfiguredTokenFilters()) { - if (disallowedFilters.contains(tf.getName())) { - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, - "Expected exception for factory " + tf.getName(), () -> { - tf.get(idxSettings, null, tf.getName(), settings).getSynonymFilter(); - }); - assertEquals(tf.getName(), "Token filter [" + tf.getName() - + "] cannot be used to parse synonyms", - e.getMessage()); - } - else { - tf.get(idxSettings, null, tf.getName(), settings).getSynonymFilter(); + Set disallowedFiltersTested = new HashSet(); + + try (CommonAnalysisPlugin plugin = new CommonAnalysisPlugin()) { + for (PreConfiguredTokenFilter tf : plugin.getPreConfiguredTokenFilters()) { + if (disallowedFilters.contains(tf.getName())) { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + "Expected exception for factory " + tf.getName(), () -> { + tf.get(idxSettings, null, tf.getName(), settings).getSynonymFilter(); + }); + assertEquals(tf.getName(), "Token filter [" + tf.getName() + "] cannot be used to parse synonyms", e.getMessage()); + disallowedFiltersTested.add(tf.getName()); + } else { + tf.get(idxSettings, null, tf.getName(), settings).getSynonymFilter(); + } } } + assertEquals("Set of dissallowed filters contains more filters than tested", disallowedFiltersTested, disallowedFilters); } public void testDisallowedTokenFilters() throws IOException { From 2083266e9df4e12e44da9ce58e824e82bcf60c19 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Thu, 2 Jan 2020 13:13:00 -0500 Subject: [PATCH 356/686] [DOCS] Correct typos in Painless datetime docs (#50563) Fixes several typos and grammar errors raised by @glenacota in #47512. Co-authored-by: Guido Lena Cota --- .../painless/painless-guide/painless-datetime.asciidoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/painless/painless-guide/painless-datetime.asciidoc b/docs/painless/painless-guide/painless-datetime.asciidoc index 227119867b67c..24d5b56ab6c0c 100644 --- a/docs/painless/painless-guide/painless-datetime.asciidoc +++ b/docs/painless/painless-guide/painless-datetime.asciidoc @@ -45,7 +45,7 @@ datetime formatting is a switch from a complex datetime to a string datetime. A <> is a complex type (<>) that defines the allowed sequence of characters for a string datetime. Datetime parsing and formatting often -requires a DateTimeFormatter. For more information about how to use a +require a DateTimeFormatter. For more information about how to use a DateTimeFormatter see the {java11-javadoc}/java.base/java/time/format/DateTimeFormatter.html[Java documentation]. @@ -231,8 +231,8 @@ ZonedDateTime updatedZdt = zdt.withYear(1976); Use either two numeric datetimes or two complex datetimes to calculate the difference (elapsed time) between two different datetimes. Use -<> to calculate the difference between -between two numeric datetimes of the same time unit such as milliseconds. For +<> to calculate the difference between two +numeric datetimes of the same time unit such as milliseconds. For complex datetimes there is often a method or another complex type (<>) available to calculate the difference. Use <> @@ -606,9 +606,9 @@ value for the current document. ==== Datetime Now Under most Painless contexts the current datetime, `now`, is not supported. -There are two primary reasons for this. The first is scripts are often run once +There are two primary reasons for this. The first is that scripts are often run once per document, so each time the script is run a different `now` is returned. The -second is scripts are often run in a distributed fashion without a way to +second is that scripts are often run in a distributed fashion without a way to appropriately synchronize `now`. Instead, pass in a user-defined parameter with either a string datetime or numeric datetime for `now`. A numeric datetime is preferred as there is no need to parse it for comparison. From d47a53cea3167f37c96be65de3853b50fed509a4 Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Thu, 2 Jan 2020 19:19:52 +0100 Subject: [PATCH 357/686] Enhance TransportReplicationAction assertions (#49081) Include failure into assertion error when replication action discovers that it has been double triggered. --- .../support/replication/TransportReplicationAction.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java b/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java index 5888ec3f46bf6..81402ffead304 100644 --- a/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java @@ -792,7 +792,7 @@ void finishAsFailed(Exception failure) { logger.trace(() -> new ParameterizedMessage("operation failed. action [{}], request [{}]", actionName, request), failure); listener.onFailure(failure); } else { - assert false : "finishAsFailed called but operation is already finished"; + assert false : new AssertionError("finishAsFailed called but operation is already finished", failure); } } @@ -804,7 +804,7 @@ void finishWithUnexpectedFailure(Exception failure) { setPhase(task, "failed"); listener.onFailure(failure); } else { - assert false : "finishWithUnexpectedFailure called but operation is already finished"; + assert false : new AssertionError("finishWithUnexpectedFailure called but operation is already finished", failure); } } From 300b02a3ad37f7811f4339bd6bc143fec56ab61f Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 2 Jan 2020 10:59:54 -0800 Subject: [PATCH 358/686] [DOCS] Adds filter and calendar attributes (#50566) --- .../anomaly-detection/apis/close-job.asciidoc | 2 -- .../apis/delete-calendar-event.asciidoc | 3 ++- .../apis/delete-calendar-job.asciidoc | 3 ++- .../apis/delete-calendar.asciidoc | 3 ++- .../apis/delete-filter.asciidoc | 3 ++- .../apis/eventresource.asciidoc | 27 ------------------- .../apis/filterresource.asciidoc | 17 ------------ .../apis/get-calendar-event.asciidoc | 19 ++++++------- .../apis/get-calendar.asciidoc | 6 +++-- .../apis/get-filter.asciidoc | 6 +++-- .../apis/post-calendar-event.asciidoc | 23 ++++++++-------- .../apis/put-calendar-job.asciidoc | 3 ++- .../apis/put-calendar.asciidoc | 5 ++-- .../apis/put-filter.asciidoc | 3 ++- .../apis/update-filter.asciidoc | 3 ++- docs/reference/ml/ml-shared.asciidoc | 8 ++++++ 16 files changed, 55 insertions(+), 79 deletions(-) delete mode 100644 docs/reference/ml/anomaly-detection/apis/eventresource.asciidoc delete mode 100644 docs/reference/ml/anomaly-detection/apis/filterresource.asciidoc diff --git a/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc index 0700d92ba7283..2520ae95faeb5 100644 --- a/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/close-job.asciidoc @@ -88,8 +88,6 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=allow-no-jobs] [[ml-close-job-example]] ==== {api-examples-title} -The following example closes the `total-requests` job: - [source,console] -------------------------------------------------- POST _ml/anomaly_detectors/low_request_rate/_close diff --git a/docs/reference/ml/anomaly-detection/apis/delete-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-calendar-event.asciidoc index d401d7dc052d4..e7bdbd1a02867 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-calendar-event.asciidoc @@ -31,7 +31,8 @@ events and delete the calendar, see the ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the calendar. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] ``:: (Required, string) Identifier for the scheduled event. You can obtain this diff --git a/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc index 12f7b88769dd8..adf0907118175 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-calendar-job.asciidoc @@ -24,7 +24,8 @@ Deletes {anomaly-jobs} from a calendar. ==== {api-path-parms-title} ``:: -(Required, string) Identifier for the calendar. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] ``:: (Required, string) diff --git a/docs/reference/ml/anomaly-detection/apis/delete-calendar.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-calendar.asciidoc index e9ac45e35600a..86e9d43591d21 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-calendar.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-calendar.asciidoc @@ -30,7 +30,8 @@ calendar. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the calendar. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] [[ml-delete-calendar-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc index 2894aaefb7a89..e691cfea4a159 100644 --- a/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/delete-filter.asciidoc @@ -31,7 +31,8 @@ update or delete the job before you can delete the filter. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the filter. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=filter-id] [[ml-delete-filter-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/eventresource.asciidoc b/docs/reference/ml/anomaly-detection/apis/eventresource.asciidoc deleted file mode 100644 index 2f3ea4175936a..0000000000000 --- a/docs/reference/ml/anomaly-detection/apis/eventresource.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[role="xpack"] -[testenv="platinum"] -[[ml-event-resource]] -=== Scheduled event resources - -An events resource has the following properties: - -`calendar_id`:: - (string) An identifier for the calendar that contains the scheduled - event. This property is optional in the <>. - -`description`:: - (string) A description of the scheduled event. - -`end_time`:: - (date) The timestamp for the end of the scheduled event - in milliseconds since the epoch or ISO 8601 format. - -`event_id`:: - (string) An automatically-generated identifier for the scheduled event. - -`start_time`:: - (date) The timestamp for the beginning of the scheduled event - in milliseconds since the epoch or ISO 8601 format. - -For more information, see -{ml-docs}/ml-calendars.html[Calendars and scheduled events]. diff --git a/docs/reference/ml/anomaly-detection/apis/filterresource.asciidoc b/docs/reference/ml/anomaly-detection/apis/filterresource.asciidoc deleted file mode 100644 index 39d63aec6a418..0000000000000 --- a/docs/reference/ml/anomaly-detection/apis/filterresource.asciidoc +++ /dev/null @@ -1,17 +0,0 @@ -[role="xpack"] -[testenv="platinum"] -[[ml-filter-resource]] -=== Filter resources - -A filter resource has the following properties: - -`filter_id`:: - (string) A string that uniquely identifies the filter. - -`description`:: - (string) A description of the filter. - -`items`:: - (array of strings) An array of strings which is the filter item list. - -For more information, see {ml-docs}/ml-rules.html[Machine learning custom rules]. diff --git a/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc index 22fdc718c5d8e..5c9f6a01acd1d 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-calendar-event.asciidoc @@ -35,7 +35,8 @@ For more information, see ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the calendar. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] [[ml-get-calendar-event-request-body]] ==== {api-request-body-title} @@ -60,22 +61,22 @@ The API returns an array of scheduled event resources, which have the following properties: `calendar_id`:: - (string) An identifier for the calendar that contains the scheduled - event. +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] `description`:: - (string) A description of the scheduled event. +(string) A description of the scheduled event. `end_time`:: - (date) The timestamp for the end of the scheduled event - in milliseconds since the epoch or ISO 8601 format. +(date) The timestamp for the end of the scheduled event in milliseconds since +the epoch or ISO 8601 format. `event_id`:: - (string) An automatically-generated identifier for the scheduled event. +(string) An automatically-generated identifier for the scheduled event. `start_time`:: - (date) The timestamp for the beginning of the scheduled event - in milliseconds since the epoch or ISO 8601 format. +(date) The timestamp for the beginning of the scheduled event in milliseconds +since the epoch or ISO 8601 format. [[ml-get-calendar-event-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc index 9c56a7742de5b..caf720547bf8e 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-calendar.asciidoc @@ -35,7 +35,8 @@ For more information, see ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the calendar. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] [[ml-get-calendar-request-body]] ==== {api-request-body-title} @@ -53,7 +54,8 @@ The API returns an array of calendar resources, which have the following properties: `calendar_id`:: -(string) A numerical character string that uniquely identifies the calendar. +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] `job_ids`:: (array) An array of {anomaly-job} identifiers. For example: diff --git a/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc index 9da2f44c198c7..59b4fcccb89e5 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-filter.asciidoc @@ -32,7 +32,8 @@ You can get a single filter or all filters. For more information, see ==== {api-path-parms-title} ``:: - (Optional, string) Identifier for the filter. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=filter-id] [[ml-get-filter-query-parms]] ==== {api-query-parms-title} @@ -53,7 +54,8 @@ properties: (string) A description of the filter. `filter_id`:: -(string) A string that uniquely identifies the filter. +(string) +include::{docdir}/ml/ml-shared.asciidoc[tag=filter-id] `items`:: (array of strings) An array of strings which is the filter item list. diff --git a/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc b/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc index 76e7111fcd0cb..f981f24badca9 100644 --- a/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/post-calendar-event.asciidoc @@ -30,30 +30,31 @@ of which must have a start time, end time, and description. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the calendar. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] [[ml-post-calendar-event-request-body]] ==== {api-request-body-title} `events`:: - (Required, array) A list of one of more scheduled events. The event's start - and end times may be specified as integer milliseconds since the epoch or as a - string in ISO 8601 format. An event resource has the following properties: +(Required, array) A list of one of more scheduled events. The event's start and +end times may be specified as integer milliseconds since the epoch or as a +string in ISO 8601 format. An event resource has the following properties: `events`.`calendar_id`::: - (Optional, string) An identifier for the calendar that contains the scheduled - event. +(Optional, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] `events`.`description`::: - (Optional, string) A description of the scheduled event. +(Optional, string) A description of the scheduled event. `events`.`end_time`::: - (Required, date) The timestamp for the end of the scheduled event - in milliseconds since the epoch or ISO 8601 format. +(Required, date) The timestamp for the end of the scheduled event in +milliseconds since the epoch or ISO 8601 format. `events`.`start_time`::: - (Required, date) The timestamp for the beginning of the scheduled event - in milliseconds since the epoch or ISO 8601 format. +(Required, date) The timestamp for the beginning of the scheduled event in +milliseconds since the epoch or ISO 8601 format. [[ml-post-calendar-event-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/put-calendar-job.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-calendar-job.asciidoc index 767a3d3d5bae5..650ed1a8878fb 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-calendar-job.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-calendar-job.asciidoc @@ -24,7 +24,8 @@ Adds an {anomaly-job} to a calendar. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the calendar. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] ``:: (Required, string) diff --git a/docs/reference/ml/anomaly-detection/apis/put-calendar.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-calendar.asciidoc index f543c06bd8cb0..5b7ada0f9b172 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-calendar.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-calendar.asciidoc @@ -30,13 +30,14 @@ For more information, see ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the calendar. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=calendar-id] [[ml-put-calendar-request-body]] ==== {api-request-body-title} `description`:: - (Optional, string) A description of the calendar. +(Optional, string) A description of the calendar. [[ml-put-calendar-example]] ==== {api-examples-title} diff --git a/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc index 3168389690a75..47f3af0909193 100644 --- a/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/put-filter.asciidoc @@ -31,7 +31,8 @@ the `custom_rules` property of detector configuration objects. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the filter. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=filter-id] [[ml-put-filter-request-body]] ==== {api-request-body-title} diff --git a/docs/reference/ml/anomaly-detection/apis/update-filter.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-filter.asciidoc index bf55d6900b1ec..66aa9ad8dad02 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-filter.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-filter.asciidoc @@ -24,7 +24,8 @@ Updates the description of a filter, adds items, or removes items. ==== {api-path-parms-title} ``:: - (Required, string) Identifier for the filter. +(Required, string) +include::{docdir}/ml/ml-shared.asciidoc[tag=filter-id] [[ml-update-filter-request-body]] ==== {api-request-body-title} diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index 6918a3a502796..7b0f8b64718a7 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -140,6 +140,10 @@ analyzing the splits with respect to their own history. It is used for finding unusual values in the context of the split. end::by-field-name[] +tag::calendar-id[] +A string that uniquely identifies a calendar. +end::calendar-id[] + tag::categorization-analyzer[] If `categorization_field_name` is specified, you can also define the analyzer that is used to interpret the categorization field. This property cannot be used @@ -570,6 +574,10 @@ optional. If it is not specified, no token filters are applied prior to categorization. end::filter[] +tag::filter-id[] +A string that uniquely identifies a filter. +end::filter-id[] + tag::frequency[] The interval at which scheduled queries are made while the {dfeed} runs in real time. The default value is either the bucket span for short bucket spans, or, From 5d5fe811b8c16ff6152f63cbc7eae13ed6737f4e Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Thu, 2 Jan 2020 16:39:18 -0500 Subject: [PATCH 359/686] Mark some constants in decay functions final (#50569) This marks a couple of constants in the `DecayFunctionBuilder` as final. They are written in CONSTANT_CASE and used as constants but not final which is a little confusing and might lead to sneaky bugs. --- .../index/query/functionscore/DecayFunctionBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java index 7bca4ac920683..e31e646200a69 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java @@ -65,8 +65,8 @@ public abstract class DecayFunctionBuilder protected static final String DECAY = "decay"; protected static final String OFFSET = "offset"; - public static double DEFAULT_DECAY = 0.5; - public static MultiValueMode DEFAULT_MULTI_VALUE_MODE = MultiValueMode.MIN; + public static final double DEFAULT_DECAY = 0.5; + public static final MultiValueMode DEFAULT_MULTI_VALUE_MODE = MultiValueMode.MIN; private final String fieldName; //parsing of origin, scale, offset and decay depends on the field type, delayed to the data node that has the mapping for it From 76355c7098827b08f576b82e5722d0cb349a4cda Mon Sep 17 00:00:00 2001 From: bellengao Date: Fri, 3 Jan 2020 07:10:21 +0800 Subject: [PATCH 360/686] Don't dump a stacktrace for invalid patterns when executing elasticsearch-croneval (#49744) --- docs/reference/commands/croneval.asciidoc | 6 +- .../trigger/schedule/tool/CronEvalTool.java | 90 +++++++++++-------- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/docs/reference/commands/croneval.asciidoc b/docs/reference/commands/croneval.asciidoc index be9b16770dc33..fa7b9aed33bb3 100644 --- a/docs/reference/commands/croneval.asciidoc +++ b/docs/reference/commands/croneval.asciidoc @@ -30,7 +30,11 @@ This command is provided in the `$ES_HOME/bin` directory. `-c, --count` :: The number of future times this expression will be triggered. The default value is `10`. - + +`-d, --detail`:: + Shows detail for invalid cron expression. It will print the stacktrace if the + expression is not valid. + `-h, --help`:: Returns all of the command parameters. diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java index 565bae15ea998..9464abcc4ba59 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java @@ -37,13 +37,16 @@ public static void main(String[] args) throws Exception { private final OptionSpec countOption; private final OptionSpec arguments; + private final OptionSpec detailOption; CronEvalTool() { super("Validates and evaluates a cron expression"); - this.countOption = parser.acceptsAll(Arrays.asList("c", "count"), - "The number of future times this expression will be triggered") - .withRequiredArg().ofType(Integer.class).defaultsTo(10); + this.countOption = parser.acceptsAll(Arrays.asList("c", "count"), "The number of future times this expression will be triggered") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(10); this.arguments = parser.nonOptions("expression"); + this.detailOption = parser.acceptsAll(Arrays.asList("d", "detail"), "Show detail for invalid cron expression"); parser.accepts("E", "Unused. Only for compatibility with other CLI tools.").withRequiredArg(); } @@ -55,46 +58,63 @@ protected void execute(Terminal terminal, OptionSet options) throws Exception { if (args.size() != 1) { throw new UserException(ExitCodes.USAGE, "expecting a single argument that is the cron expression to evaluate"); } - execute(terminal, args.get(0), count); + boolean printDetail = options.has(detailOption); + execute(terminal, args.get(0), count, printDetail); } - private void execute(Terminal terminal, String expression, int count) throws Exception { - Cron.validate(expression); - terminal.println("Valid!"); + private void execute(Terminal terminal, String expression, int count, boolean printDetail) throws Exception { + try { + Cron.validate(expression); + terminal.println("Valid!"); - final ZonedDateTime date = ZonedDateTime.now(ZoneOffset.UTC); - final boolean isLocalTimeUTC = UTC_FORMATTER.zone().equals(LOCAL_FORMATTER.zone()); - if (isLocalTimeUTC) { - terminal.println("Now is [" + UTC_FORMATTER.format(date) + "] in UTC"); - } else { - terminal.println("Now is [" + UTC_FORMATTER.format(date) + "] in UTC, local time is [" + LOCAL_FORMATTER.format(date) + "]"); + final ZonedDateTime date = ZonedDateTime.now(ZoneOffset.UTC); + final boolean isLocalTimeUTC = UTC_FORMATTER.zone().equals(LOCAL_FORMATTER.zone()); + if (isLocalTimeUTC) { + terminal.println("Now is [" + UTC_FORMATTER.format(date) + "] in UTC"); + } else { + terminal.println( + "Now is [" + UTC_FORMATTER.format(date) + "] in UTC, local time is [" + LOCAL_FORMATTER.format(date) + "]" + ); - } - terminal.println("Here are the next " + count + " times this cron expression will trigger:"); - - Cron cron = new Cron(expression); - long time = date.toInstant().toEpochMilli(); - - for (int i = 0; i < count; i++) { - long prevTime = time; - time = cron.getNextValidTimeAfter(time); - if (time < 0) { - if (i == 0) { - throw new UserException(ExitCodes.OK, "Could not compute future times since [" - + UTC_FORMATTER.format(Instant.ofEpochMilli(prevTime)) + "] " + "(perhaps the cron expression only points to " + - "times in the" + - " " + - "past?)"); - } - break; } + terminal.println("Here are the next " + count + " times this cron expression will trigger:"); + + Cron cron = new Cron(expression); + long time = date.toInstant().toEpochMilli(); + + for (int i = 0; i < count; i++) { + long prevTime = time; + time = cron.getNextValidTimeAfter(time); + if (time < 0) { + if (i == 0) { + throw new UserException( + ExitCodes.OK, + "Could not compute future times since [" + + UTC_FORMATTER.format(Instant.ofEpochMilli(prevTime)) + + "] " + + "(perhaps the cron expression only points to " + + "times in the" + + " " + + "past?)" + ); + } + break; + } - if (isLocalTimeUTC) { - terminal.println((i + 1) + ".\t" + UTC_FORMATTER.format(Instant.ofEpochMilli(time))); + if (isLocalTimeUTC) { + terminal.println((i + 1) + ".\t" + UTC_FORMATTER.format(Instant.ofEpochMilli(time))); + } else { + terminal.println((i + 1) + ".\t" + UTC_FORMATTER.format(Instant.ofEpochMilli(time))); + terminal.println("\t" + LOCAL_FORMATTER.format(Instant.ofEpochMilli(time))); + } + } + } catch (Exception e) { + if (printDetail) { + throw e; } else { - terminal.println((i + 1) + ".\t" + UTC_FORMATTER.format(Instant.ofEpochMilli(time))); - terminal.println("\t" + LOCAL_FORMATTER.format(Instant.ofEpochMilli(time))); + throw new UserException(ExitCodes.OK, e.getMessage() + (e.getCause() == null ? "" : ": " + e.getCause().getMessage())); } + } } } From 96c8899b05320cc2a5c2fabb2199d7f50d5bb18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Fri, 3 Jan 2020 10:41:38 +0100 Subject: [PATCH 361/686] [DOCS] Specifies the possible data types of classification dependent_variable (#50582) --- docs/reference/ml/df-analytics/apis/analysisobjects.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/ml/df-analytics/apis/analysisobjects.asciidoc b/docs/reference/ml/df-analytics/apis/analysisobjects.asciidoc index 035601fe71409..0004416c05cd7 100644 --- a/docs/reference/ml/df-analytics/apis/analysisobjects.asciidoc +++ b/docs/reference/ml/df-analytics/apis/analysisobjects.asciidoc @@ -145,7 +145,8 @@ include::{docdir}/ml/ml-shared.asciidoc[tag=lambda] include::{docdir}/ml/ml-shared.asciidoc[tag=dependent-variable] + -- -The data type of the field must be numeric or boolean. +The data type of the field must be numeric (`integer`, `short`, `long`, `byte`), +categorical (`ip`, `keyword`, `text`), or boolean. -- `num_top_classes`:: From f38555b4825f7216e88e87a67946839136b968a7 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Fri, 3 Jan 2020 09:55:53 +0000 Subject: [PATCH 362/686] Add fuzzy intervals source (#49762) This intervals source will return terms that are similar to an input term, up to an edit distance defined by fuzziness, similar to FuzzyQuery. Closes #49595 --- .../query-dsl/intervals-query.asciidoc | 39 ++++- .../test/search/230_interval_query.yml | 21 +++ .../org/apache/lucene/queries/XIntervals.java | 4 + .../index/query/IntervalsSourceProvider.java | 149 ++++++++++++++++++ .../elasticsearch/search/SearchModule.java | 2 + .../query/IntervalQueryBuilderTests.java | 58 +++++++ 6 files changed, 272 insertions(+), 1 deletion(-) diff --git a/docs/reference/query-dsl/intervals-query.asciidoc b/docs/reference/query-dsl/intervals-query.asciidoc index 9f9280c80a484..7fc9d60b26397 100644 --- a/docs/reference/query-dsl/intervals-query.asciidoc +++ b/docs/reference/query-dsl/intervals-query.asciidoc @@ -73,6 +73,7 @@ Valid rules include: * <> * <> * <> +* <> * <> * <> -- @@ -97,7 +98,7 @@ set to `0`, the terms must appear next to each other. -- `ordered`:: -(Optional, boolean) +(Optional, boolean) If `true`, matching terms must appear in their specified order. Defaults to `false`. @@ -177,6 +178,42 @@ The `pattern` is normalized using the search analyzer from this field, unless `analyzer` is specified separately. -- +[[intervals-fuzzy]] +==== `fuzzy` rule parameters + +The `fuzzy` rule matches terms that are similar to the provided term, within an +edit distance defined by <>. If the fuzzy expansion matches more than +128 terms, {es} returns an error. + +`term`:: +(Required, string) The term to match + +`prefix_length`:: +(Optional, string) Number of beginning characters left unchanged when creating +expansions. Defaults to `0`. + +`transpositions`:: +(Optional, boolean) Indicates whether edits include transpositions of two +adjacent characters (ab → ba). Defaults to `true`. + +`fuzziness`:: +(Optional, string) Maximum edit distance allowed for matching. See <> +for valid values and more information. Defaults to `auto`. + +`analyzer`:: +(Optional, string) <> used to normalize the `term`. +Defaults to the top-level `` 's analyzer. + +`use_field`:: ++ +-- +(Optional, string) If specified, match intervals from this field rather than the +top-level ``. + +The `term` is normalized using the search analyzer from this field, unless +`analyzer` is specified separately. +-- + [[intervals-all_of]] ==== `all_of` rule parameters diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/230_interval_query.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/230_interval_query.yml index 82aa0883008a8..654ef2a2e173f 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/230_interval_query.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/230_interval_query.yml @@ -424,3 +424,24 @@ setup: pattern: out?ide - match: { hits.total.value: 3 } +--- +"Test fuzzy match": + - skip: + version: " - 8.0.0" + reason: "TODO: change to 7.6 in backport" + - do: + search: + index: test + body: + query: + intervals: + text: + all_of: + intervals: + - fuzzy: + query: cald + - prefix: + prefix: out + - match: { hits.total.value: 3 } + + diff --git a/server/src/main/java/org/apache/lucene/queries/XIntervals.java b/server/src/main/java/org/apache/lucene/queries/XIntervals.java index b389a29c21115..1d77094bd1427 100644 --- a/server/src/main/java/org/apache/lucene/queries/XIntervals.java +++ b/server/src/main/java/org/apache/lucene/queries/XIntervals.java @@ -67,6 +67,10 @@ public static IntervalsSource prefix(BytesRef prefix) { return new MultiTermIntervalsSource(ca, 128, prefix.utf8ToString()); } + public static IntervalsSource multiterm(CompiledAutomaton ca, String label) { + return new MultiTermIntervalsSource(ca, 128, label); + } + static class MultiTermIntervalsSource extends IntervalsSource { private final CompiledAutomaton automaton; diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java index 4918d7c7c7f3f..dbd8f339ca66f 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java @@ -20,12 +20,15 @@ package org.elasticsearch.index.query; import org.apache.lucene.index.IndexOptions; +import org.apache.lucene.index.Term; import org.apache.lucene.queries.XIntervals; import org.apache.lucene.queries.intervals.FilteredIntervalsSource; import org.apache.lucene.queries.intervals.IntervalIterator; import org.apache.lucene.queries.intervals.Intervals; import org.apache.lucene.queries.intervals.IntervalsSource; +import org.apache.lucene.search.FuzzyQuery; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.CompiledAutomaton; import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; @@ -33,7 +36,9 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -85,6 +90,8 @@ public static IntervalsSourceProvider fromXContent(XContentParser parser) throws return Prefix.fromXContent(parser); case "wildcard": return Wildcard.fromXContent(parser); + case "fuzzy": + return Fuzzy.fromXContent(parser); } throw new ParsingException(parser.getTokenLocation(), "Unknown interval type [" + parser.currentName() + "], expecting one of [match, any_of, all_of, prefix, wildcard]"); @@ -691,6 +698,148 @@ String getUseField() { } } + public static class Fuzzy extends IntervalsSourceProvider { + + public static final String NAME = "fuzzy"; + + private final String term; + private final int prefixLength; + private final boolean transpositions; + private final Fuzziness fuzziness; + private final String analyzer; + private final String useField; + + public Fuzzy(String term, int prefixLength, boolean transpositions, Fuzziness fuzziness, String analyzer, String useField) { + this.term = term; + this.prefixLength = prefixLength; + this.transpositions = transpositions; + this.fuzziness = fuzziness; + this.analyzer = analyzer; + this.useField = useField; + } + + public Fuzzy(StreamInput in) throws IOException { + this.term = in.readString(); + this.prefixLength = in.readVInt(); + this.transpositions = in.readBoolean(); + this.fuzziness = new Fuzziness(in); + this.analyzer = in.readOptionalString(); + this.useField = in.readOptionalString(); + } + + @Override + public IntervalsSource getSource(QueryShardContext context, MappedFieldType fieldType) { + NamedAnalyzer analyzer = fieldType.searchAnalyzer(); + if (this.analyzer != null) { + analyzer = context.getMapperService().getIndexAnalyzers().get(this.analyzer); + } + IntervalsSource source; + if (useField != null) { + fieldType = context.fieldMapper(useField); + assert fieldType != null; + checkPositions(fieldType); + if (this.analyzer == null) { + analyzer = fieldType.searchAnalyzer(); + } + } + checkPositions(fieldType); + BytesRef normalizedTerm = analyzer.normalize(fieldType.name(), term); + FuzzyQuery fq = new FuzzyQuery(new Term(fieldType.name(), normalizedTerm), + fuzziness.asDistance(term), prefixLength, 128, transpositions); + CompiledAutomaton ca = new CompiledAutomaton(fq.toAutomaton()); + source = XIntervals.multiterm(ca, term); + if (useField != null) { + source = Intervals.fixField(useField, source); + } + return source; + } + + private void checkPositions(MappedFieldType type) { + if (type.indexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) < 0) { + throw new IllegalArgumentException("Cannot create intervals over field [" + type.name() + "] with no positions indexed"); + } + } + + @Override + public void extractFields(Set fields) { + if (useField != null) { + fields.add(useField); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Fuzzy fuzzy = (Fuzzy) o; + return prefixLength == fuzzy.prefixLength && + transpositions == fuzzy.transpositions && + Objects.equals(term, fuzzy.term) && + Objects.equals(fuzziness, fuzzy.fuzziness) && + Objects.equals(analyzer, fuzzy.analyzer) && + Objects.equals(useField, fuzzy.useField); + } + + @Override + public int hashCode() { + return Objects.hash(term, prefixLength, transpositions, fuzziness, analyzer, useField); + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(term); + out.writeVInt(prefixLength); + out.writeBoolean(transpositions); + fuzziness.writeTo(out); + out.writeOptionalString(analyzer); + out.writeOptionalString(useField); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(NAME); + builder.field("term", term); + builder.field("prefix_length", prefixLength); + builder.field("transpositions", transpositions); + fuzziness.toXContent(builder, params); + if (analyzer != null) { + builder.field("analyzer", analyzer); + } + if (useField != null) { + builder.field("use_field", useField); + } + builder.endObject(); + return builder; + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, args -> { + String term = (String) args[0]; + int prefixLength = (args[1] == null) ? FuzzyQueryBuilder.DEFAULT_PREFIX_LENGTH : (int) args[1]; + boolean transpositions = (args[2] == null) ? FuzzyQueryBuilder.DEFAULT_TRANSPOSITIONS : (boolean) args[2]; + Fuzziness fuzziness = (args[3] == null) ? FuzzyQueryBuilder.DEFAULT_FUZZINESS : (Fuzziness) args[3]; + String analyzer = (String) args[4]; + String useField = (String) args[5]; + return new Fuzzy(term, prefixLength, transpositions, fuzziness, analyzer, useField); + }); + static { + PARSER.declareString(constructorArg(), new ParseField("term")); + PARSER.declareInt(optionalConstructorArg(), new ParseField("prefix_length")); + PARSER.declareBoolean(optionalConstructorArg(), new ParseField("transpositions")); + PARSER.declareField(optionalConstructorArg(), (p, c) -> Fuzziness.parse(p), Fuzziness.FIELD, ObjectParser.ValueType.VALUE); + PARSER.declareString(optionalConstructorArg(), new ParseField("analyzer")); + PARSER.declareString(optionalConstructorArg(), new ParseField("use_field")); + } + + public static Fuzzy fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + } + static class ScriptFilterSource extends FilteredIntervalsSource { final IntervalFilterScript script; diff --git a/server/src/main/java/org/elasticsearch/search/SearchModule.java b/server/src/main/java/org/elasticsearch/search/SearchModule.java index cdfd28760869c..ca64b749a5809 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/server/src/main/java/org/elasticsearch/search/SearchModule.java @@ -804,6 +804,8 @@ private void registerIntervalsSourceProviders() { IntervalsSourceProvider.Prefix.NAME, IntervalsSourceProvider.Prefix::new)); namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, IntervalsSourceProvider.Wildcard.NAME, IntervalsSourceProvider.Wildcard::new)); + namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, + IntervalsSourceProvider.Fuzzy.NAME, IntervalsSourceProvider.Fuzzy::new)); } private void registerQuery(QuerySpec spec) { diff --git a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java index 763d10ddf30e8..ed7caeb0473de 100644 --- a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java @@ -19,17 +19,22 @@ package org.elasticsearch.index.query; +import org.apache.lucene.index.Term; import org.apache.lucene.queries.XIntervals; import org.apache.lucene.queries.intervals.IntervalQuery; import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; import org.apache.lucene.search.BoostQuery; +import org.apache.lucene.search.FuzzyQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.CompiledAutomaton; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.mapper.MapperService; @@ -529,4 +534,57 @@ public void testWildcard() throws IOException { assertEquals(expected, builder.toQuery(createShardContext())); } + private static IntervalsSource buildFuzzySource(String term, String label, int prefixLength, boolean transpositions, int editDistance) { + FuzzyQuery fq = new FuzzyQuery(new Term("field", term), editDistance, prefixLength, 128, transpositions); + return XIntervals.multiterm(new CompiledAutomaton(fq.toAutomaton()), label); + } + + public void testFuzzy() throws IOException { + + String json = "{ \"intervals\" : { \"" + STRING_FIELD_NAME + "\": { " + + "\"fuzzy\" : { \"term\" : \"Term\" } } } }"; + IntervalQueryBuilder builder = (IntervalQueryBuilder) parseQuery(json); + + Query expected = new IntervalQuery(STRING_FIELD_NAME, + buildFuzzySource("term", "Term", FuzzyQueryBuilder.DEFAULT_PREFIX_LENGTH, true, Fuzziness.AUTO.asDistance("term"))); + assertEquals(expected, builder.toQuery(createShardContext())); + + String json_with_prefix = "{ \"intervals\" : { \"" + STRING_FIELD_NAME + "\": { " + + "\"fuzzy\" : { \"term\" : \"Term\", \"prefix_length\" : 2 } } } }"; + builder = (IntervalQueryBuilder) parseQuery(json_with_prefix); + expected = new IntervalQuery(STRING_FIELD_NAME, + buildFuzzySource("term", "Term", 2, true, Fuzziness.AUTO.asDistance("term"))); + assertEquals(expected, builder.toQuery(createShardContext())); + + String json_with_fuzziness = "{ \"intervals\" : { \"" + STRING_FIELD_NAME + "\": { " + + "\"fuzzy\" : { \"term\" : \"Term\", \"prefix_length\" : 2, \"fuzziness\" : \"1\" } } } }"; + builder = (IntervalQueryBuilder) parseQuery(json_with_fuzziness); + expected = new IntervalQuery(STRING_FIELD_NAME, + buildFuzzySource("term", "Term", 2, true, Fuzziness.ONE.asDistance("term"))); + assertEquals(expected, builder.toQuery(createShardContext())); + + String json_no_transpositions = "{ \"intervals\" : { \"" + STRING_FIELD_NAME + "\": { " + + "\"fuzzy\" : { \"term\" : \"Term\", \"prefix_length\" : 2, \"transpositions\" : false } } } }"; + builder = (IntervalQueryBuilder) parseQuery(json_no_transpositions); + expected = new IntervalQuery(STRING_FIELD_NAME, + buildFuzzySource("term", "Term", 2, false, Fuzziness.AUTO.asDistance("term"))); + assertEquals(expected, builder.toQuery(createShardContext())); + + String json_with_analyzer = "{ \"intervals\" : { \"" + STRING_FIELD_NAME + "\": { " + + "\"fuzzy\" : { \"term\" : \"Term\", \"prefix_length\" : 2, \"analyzer\" : \"keyword\" } } } }"; + builder = (IntervalQueryBuilder) parseQuery(json_with_analyzer); + expected = new IntervalQuery(STRING_FIELD_NAME, + buildFuzzySource("Term", "Term", 2, true, Fuzziness.AUTO.asDistance("term"))); + assertEquals(expected, builder.toQuery(createShardContext())); + + String json_with_fixfield = "{ \"intervals\" : { \"" + STRING_FIELD_NAME + "\": { " + + "\"fuzzy\" : { \"term\" : \"Term\", \"prefix_length\" : 2, \"fuzziness\" : \"1\", " + + "\"use_field\" : \"" + MASKED_FIELD + "\" } } } }"; + builder = (IntervalQueryBuilder) parseQuery(json_with_fixfield); + expected = new IntervalQuery(STRING_FIELD_NAME, Intervals.fixField(MASKED_FIELD, + buildFuzzySource("term", "Term", 2, true, Fuzziness.ONE.asDistance("term")))); + assertEquals(expected, builder.toQuery(createShardContext())); + + } + } From e15470a47d3881928b0ab1ffd75e37add3f4b654 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 3 Jan 2020 12:01:41 +0200 Subject: [PATCH 363/686] [ML] Implement force deleting a data frame analytics job (#50553) Adds a `force` parameter to the delete data frame analytics request. When `force` is `true`, the action force-stops the jobs and then proceeds to the deletion. This can be used in order to delete a non-stopped job with a single request. Closes #48124 --- .../client/MLRequestConverters.java | 15 ++++++-- .../ml/DeleteDataFrameAnalyticsRequest.java | 19 ++++++++-- .../client/MLRequestConvertersTests.java | 13 +++++-- .../client/MachineLearningIT.java | 7 ++-- .../MlClientDocumentationIT.java | 10 ++++-- .../ml/delete-data-frame-analytics.asciidoc | 11 ++++++ .../apis/delete-dfanalytics.asciidoc | 9 ++++- .../DeleteDataFrameAnalyticsAction.java | 35 +++++++++++++------ .../core/ml/action/KillProcessAction.java | 9 ----- ...ansportDeleteDataFrameAnalyticsAction.java | 30 ++++++++++++++-- .../RestDeleteDataFrameAnalyticsAction.java | 1 + .../api/ml.delete_data_frame_analytics.json | 7 ++++ 12 files changed, 130 insertions(+), 36 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index e0bba0c78a120..4967d8091c961 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -29,8 +29,6 @@ import org.elasticsearch.client.RequestConverters.EndpointBuilder; import org.elasticsearch.client.core.PageParams; import org.elasticsearch.client.ml.CloseJobRequest; -import org.elasticsearch.client.ml.DeleteTrainedModelRequest; -import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsRequest; import org.elasticsearch.client.ml.DeleteCalendarEventRequest; import org.elasticsearch.client.ml.DeleteCalendarJobRequest; import org.elasticsearch.client.ml.DeleteCalendarRequest; @@ -41,7 +39,9 @@ import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.DeleteModelSnapshotRequest; +import org.elasticsearch.client.ml.DeleteTrainedModelRequest; import org.elasticsearch.client.ml.EvaluateDataFrameRequest; +import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsRequest; import org.elasticsearch.client.ml.FindFileStructureRequest; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.ForecastJobRequest; @@ -692,7 +692,16 @@ static Request deleteDataFrameAnalytics(DeleteDataFrameAnalyticsRequest deleteRe .addPathPartAsIs("_ml", "data_frame", "analytics") .addPathPart(deleteRequest.getId()) .build(); - return new Request(HttpDelete.METHOD_NAME, endpoint); + + Request request = new Request(HttpDelete.METHOD_NAME, endpoint); + + RequestConverters.Params params = new RequestConverters.Params(); + if (deleteRequest.getForce() != null) { + params.putParam("force", Boolean.toString(deleteRequest.getForce())); + } + request.addParameters(params.asMap()); + + return request; } static Request evaluateDataFrame(EvaluateDataFrameRequest evaluateRequest) throws IOException { diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDataFrameAnalyticsRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDataFrameAnalyticsRequest.java index f03466632304d..b7300430b55b0 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDataFrameAnalyticsRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDataFrameAnalyticsRequest.java @@ -31,6 +31,7 @@ public class DeleteDataFrameAnalyticsRequest implements Validatable { private final String id; + private Boolean force; public DeleteDataFrameAnalyticsRequest(String id) { this.id = id; @@ -40,6 +41,20 @@ public String getId() { return id; } + public Boolean getForce() { + return force; + } + + /** + * Used to forcefully delete an job that is not stopped. + * This method is quicker than stopping and deleting the job. + * + * @param force When {@code true} forcefully delete a non stopped job. Defaults to {@code false} + */ + public void setForce(Boolean force) { + this.force = force; + } + @Override public Optional validate() { if (id == null) { @@ -54,11 +69,11 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; DeleteDataFrameAnalyticsRequest other = (DeleteDataFrameAnalyticsRequest) o; - return Objects.equals(id, other.id); + return Objects.equals(id, other.id) && Objects.equals(force, other.force); } @Override public int hashCode() { - return Objects.hash(id); + return Objects.hash(id, force); } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java index cf22ba80c1624..475dda254448f 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java @@ -25,8 +25,6 @@ import org.apache.http.client.methods.HttpPut; import org.elasticsearch.client.core.PageParams; import org.elasticsearch.client.ml.CloseJobRequest; -import org.elasticsearch.client.ml.DeleteTrainedModelRequest; -import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsRequest; import org.elasticsearch.client.ml.DeleteCalendarEventRequest; import org.elasticsearch.client.ml.DeleteCalendarJobRequest; import org.elasticsearch.client.ml.DeleteCalendarRequest; @@ -37,8 +35,10 @@ import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.DeleteModelSnapshotRequest; +import org.elasticsearch.client.ml.DeleteTrainedModelRequest; import org.elasticsearch.client.ml.EvaluateDataFrameRequest; import org.elasticsearch.client.ml.EvaluateDataFrameRequestTests; +import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsRequest; import org.elasticsearch.client.ml.FindFileStructureRequest; import org.elasticsearch.client.ml.FindFileStructureRequestTests; import org.elasticsearch.client.ml.FlushJobRequest; @@ -778,6 +778,15 @@ public void testDeleteDataFrameAnalytics() { assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); assertEquals("/_ml/data_frame/analytics/" + deleteRequest.getId(), request.getEndpoint()); assertNull(request.getEntity()); + assertThat(request.getParameters().isEmpty(), is(true)); + + deleteRequest = new DeleteDataFrameAnalyticsRequest(randomAlphaOfLength(10)); + deleteRequest.setForce(true); + request = MLRequestConverters.deleteDataFrameAnalytics(deleteRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_ml/data_frame/analytics/" + deleteRequest.getId(), request.getEndpoint()); + assertNull(request.getEntity()); + assertEquals(Boolean.toString(true), request.getParameters().get("force")); } public void testEvaluateDataFrame() throws IOException { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index 443c337d089fa..f3b63801987db 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -1616,8 +1616,11 @@ public void testDeleteDataFrameAnalyticsConfig() throws Exception { machineLearningClient::getDataFrameAnalytics, machineLearningClient::getDataFrameAnalyticsAsync); assertThat(getDataFrameAnalyticsResponse.getAnalytics(), hasSize(1)); - AcknowledgedResponse deleteDataFrameAnalyticsResponse = execute( - new DeleteDataFrameAnalyticsRequest(configId), + DeleteDataFrameAnalyticsRequest deleteRequest = new DeleteDataFrameAnalyticsRequest(configId); + if (randomBoolean()) { + deleteRequest.setForce(randomBoolean()); + } + AcknowledgedResponse deleteDataFrameAnalyticsResponse = execute(deleteRequest, machineLearningClient::deleteDataFrameAnalytics, machineLearningClient::deleteDataFrameAnalyticsAsync); assertTrue(deleteDataFrameAnalyticsResponse.isAcknowledged()); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 12fea5f5c5658..37ae59e9b992a 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -36,9 +36,6 @@ import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; -import org.elasticsearch.client.ml.DeleteTrainedModelRequest; -import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsRequest; -import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsResponse; import org.elasticsearch.client.ml.DeleteCalendarEventRequest; import org.elasticsearch.client.ml.DeleteCalendarJobRequest; import org.elasticsearch.client.ml.DeleteCalendarRequest; @@ -51,8 +48,11 @@ import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.DeleteModelSnapshotRequest; +import org.elasticsearch.client.ml.DeleteTrainedModelRequest; import org.elasticsearch.client.ml.EvaluateDataFrameRequest; import org.elasticsearch.client.ml.EvaluateDataFrameResponse; +import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsRequest; +import org.elasticsearch.client.ml.ExplainDataFrameAnalyticsResponse; import org.elasticsearch.client.ml.FindFileStructureRequest; import org.elasticsearch.client.ml.FindFileStructureResponse; import org.elasticsearch.client.ml.FlushJobRequest; @@ -3065,6 +3065,10 @@ public void testDeleteDataFrameAnalytics() throws Exception { DeleteDataFrameAnalyticsRequest request = new DeleteDataFrameAnalyticsRequest("my-analytics-config"); // <1> // end::delete-data-frame-analytics-request + //tag::delete-data-frame-analytics-request-force + request.setForce(false); // <1> + //end::delete-data-frame-analytics-request-force + // tag::delete-data-frame-analytics-execute AcknowledgedResponse response = client.machineLearning().deleteDataFrameAnalytics(request, RequestOptions.DEFAULT); // end::delete-data-frame-analytics-execute diff --git a/docs/java-rest/high-level/ml/delete-data-frame-analytics.asciidoc b/docs/java-rest/high-level/ml/delete-data-frame-analytics.asciidoc index cb321d95c3864..d57b37ff21741 100644 --- a/docs/java-rest/high-level/ml/delete-data-frame-analytics.asciidoc +++ b/docs/java-rest/high-level/ml/delete-data-frame-analytics.asciidoc @@ -21,6 +21,17 @@ include-tagged::{doc-tests-file}[{api}-request] --------------------------------------------------- <1> Constructing a new request referencing an existing {dfanalytics-job}. +==== Optional arguments + +The following arguments are optional: + +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-force] +--------------------------------------------------- +<1> Use to forcefully delete a job that is not stopped. This method is quicker than stopping +and deleting the job. Defaults to `false`. + include::../execution.asciidoc[] [id="{upid}-{api}-response"] diff --git a/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc b/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc index 8d80e3bba8783..a7adad79185d2 100644 --- a/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc +++ b/docs/reference/ml/df-analytics/apis/delete-dfanalytics.asciidoc @@ -21,7 +21,7 @@ experimental[] [[ml-delete-dfanalytics-prereq]] ==== {api-prereq-title} -* You must have `machine_learning_admin` built-in role to use this API. For more +* You must have `machine_learning_admin` built-in role to use this API. For more information, see <> and <>. @@ -32,6 +32,13 @@ information, see <> and <>. (Required, string) include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-data-frame-analytics] +[[ml-delete-dfanalytics-query-params]] +==== {api-query-parms-title} + +`force`:: + (Optional, boolean) If `true`, it deletes a job that is not stopped; this method is + quicker than stopping and deleting the job. + [[ml-delete-dfanalytics-example]] ==== {api-examples-title} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDataFrameAnalyticsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDataFrameAnalyticsAction.java index c9c16025bb69c..40d70d979be9e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDataFrameAnalyticsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDataFrameAnalyticsAction.java @@ -5,16 +5,16 @@ */ package org.elasticsearch.xpack.core.ml.action; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionType; import org.elasticsearch.action.support.master.AcknowledgedRequest; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.action.support.master.MasterNodeOperationRequestBuilder; import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.ToXContentFragment; -import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; @@ -30,13 +30,21 @@ private DeleteDataFrameAnalyticsAction() { super(NAME, AcknowledgedResponse::new); } - public static class Request extends AcknowledgedRequest implements ToXContentFragment { + public static class Request extends AcknowledgedRequest { + + public static final ParseField FORCE = new ParseField("force"); private String id; + private boolean force; public Request(StreamInput in) throws IOException { super(in); id = in.readString(); + if (in.getVersion().onOrAfter(Version.CURRENT)) { + force = in.readBoolean(); + } else { + force = false; + } } public Request() {} @@ -49,15 +57,17 @@ public String getId() { return id; } - @Override - public ActionRequestValidationException validate() { - return null; + public boolean isForce() { + return force; + } + + public void setForce(boolean force) { + this.force = force; } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field(DataFrameAnalyticsConfig.ID.getPreferredName(), id); - return builder; + public ActionRequestValidationException validate() { + return null; } @Override @@ -65,18 +75,21 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DeleteDataFrameAnalyticsAction.Request request = (DeleteDataFrameAnalyticsAction.Request) o; - return Objects.equals(id, request.id); + return Objects.equals(id, request.id) && force == request.force; } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeString(id); + if (out.getVersion().onOrAfter(Version.CURRENT)) { + out.writeBoolean(force); + } } @Override public int hashCode() { - return Objects.hash(id); + return Objects.hash(id, force); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/KillProcessAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/KillProcessAction.java index dcd8c524415e7..ba7d6c1f744ea 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/KillProcessAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/KillProcessAction.java @@ -6,9 +6,7 @@ package org.elasticsearch.xpack.core.ml.action; import org.elasticsearch.action.ActionType; -import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.support.tasks.BaseTasksResponse; -import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -25,13 +23,6 @@ private KillProcessAction() { super(NAME, KillProcessAction.Response::new); } - static class RequestBuilder extends ActionRequestBuilder { - - RequestBuilder(ElasticsearchClient client, KillProcessAction action) { - super(client, action, new Request()); - } - } - public static class Request extends JobTaskRequest { public Request(String jobId) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java index d42cf9684cca9..47f2a216f2a81 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDataFrameAnalyticsAction.java @@ -38,6 +38,7 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ml.MlTasks; import org.elasticsearch.xpack.core.ml.action.DeleteDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.StopDataFrameAnalyticsAction; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsState; import org.elasticsearch.xpack.core.ml.job.messages.Messages; @@ -100,6 +101,32 @@ protected AcknowledgedResponse read(StreamInput in) throws IOException { protected void masterOperation(Task task, DeleteDataFrameAnalyticsAction.Request request, ClusterState state, ActionListener listener) { String id = request.getId(); + TaskId taskId = new TaskId(clusterService.localNode().getId(), task.getId()); + ParentTaskAssigningClient parentTaskClient = new ParentTaskAssigningClient(client, taskId); + + if (request.isForce()) { + forceDelete(parentTaskClient, id, listener); + } else { + normalDelete(parentTaskClient, state, id, listener); + } + } + + private void forceDelete(ParentTaskAssigningClient parentTaskClient, String id, + ActionListener listener) { + logger.debug("[{}] Force deleting data frame analytics job", id); + + ActionListener stopListener = ActionListener.wrap( + stopResponse -> normalDelete(parentTaskClient, clusterService.state(), id, listener), + listener::onFailure + ); + + StopDataFrameAnalyticsAction.Request request = new StopDataFrameAnalyticsAction.Request(id); + request.setForce(true); + executeAsyncWithOrigin(parentTaskClient, ML_ORIGIN, StopDataFrameAnalyticsAction.INSTANCE, request, stopListener); + } + + private void normalDelete(ParentTaskAssigningClient parentTaskClient, ClusterState state, String id, + ActionListener listener) { PersistentTasksCustomMetaData tasks = state.getMetaData().custom(PersistentTasksCustomMetaData.TYPE); DataFrameAnalyticsState taskState = MlTasks.getDataFrameAnalyticsState(id, tasks); if (taskState != DataFrameAnalyticsState.STOPPED) { @@ -108,9 +135,6 @@ protected void masterOperation(Task task, DeleteDataFrameAnalyticsAction.Request return; } - TaskId taskId = new TaskId(clusterService.localNode().getId(), task.getId()); - ParentTaskAssigningClient parentTaskClient = new ParentTaskAssigningClient(client, taskId); - // We clean up the memory tracker on delete because there is no stop; the task stops by itself memoryTracker.removeDataFrameAnalyticsJob(id); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/dataframe/RestDeleteDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/dataframe/RestDeleteDataFrameAnalyticsAction.java index 26edf340b2b16..9e78a7cd9f214 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/dataframe/RestDeleteDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/dataframe/RestDeleteDataFrameAnalyticsAction.java @@ -32,6 +32,7 @@ public String getName() { protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { String id = restRequest.param(DataFrameAnalyticsConfig.ID.getPreferredName()); DeleteDataFrameAnalyticsAction.Request request = new DeleteDataFrameAnalyticsAction.Request(id); + request.setForce(restRequest.paramAsBoolean(DeleteDataFrameAnalyticsAction.Request.FORCE.getPreferredName(), request.isForce())); return channel -> client.execute(DeleteDataFrameAnalyticsAction.INSTANCE, request, new RestToXContentListener<>(channel)); } } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_data_frame_analytics.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_data_frame_analytics.json index bb2101765c75c..98f6a3eedaba5 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_data_frame_analytics.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_data_frame_analytics.json @@ -19,6 +19,13 @@ } } ] + }, + "params":{ + "force":{ + "type":"boolean", + "description":"True if the job should be forcefully deleted", + "default":false + } } } } From ba905d7ebe147627517b1fd7e5e0d40addac0ea0 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 29 Nov 2019 14:38:06 +0200 Subject: [PATCH 364/686] [ML] Mute data frame analytics BWC tests Until #50553 is backported to 7.x --- .../test/mixed_cluster/90_ml_data_frame_analytics_crud.yml | 5 +++++ .../test/old_cluster/90_ml_data_frame_analytics_crud.yml | 3 +++ .../upgraded_cluster/90_ml_data_frame_analytics_crud.yml | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml index 7780691b2bbbd..a6fbc5cc4e2c0 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,3 +1,8 @@ +setup: + - skip: + version: "all" + reason: "Until backport of https://github.com/elastic/elasticsearch/issues/50553" + --- "Get old outlier_detection job": diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml index fe160bba15f23..51f21afa6761d 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,4 +1,7 @@ setup: + - skip: + version: "all" + reason: "Until backport of https://github.com/elastic/elasticsearch/issues/50553" - do: index: diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml index 14438883f0da1..b20c63f69d8e2 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,3 +1,8 @@ +setup: + - skip: + version: "all" + reason: "Until backport of https://github.com/elastic/elasticsearch/issues/50553" + --- "Get old cluster outlier_detection job": From 3db02d16fee9434e8fa14b7fce940543063598ec Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Fri, 3 Jan 2020 12:24:03 +0200 Subject: [PATCH 365/686] ILM retryable async action steps (#50522) This adds support for retrying AsyncActionSteps by triggering the async step after ILM was moved back on the failed step (the async step we'll be attempting to run after the cluster state reflects ILM being moved back on the failed step). This also marks the RolloverStep as retryable and adds an integration test where the RolloverStep is failing to execute as the rolled over index already exists to test that the async action RolloverStep is retried until the rolled over index is deleted. --- .../xpack/core/ilm/RolloverStep.java | 5 + .../ilm/TimeSeriesLifecycleActionsIT.java | 199 ++++++++++++------ .../xpack/ilm/IndexLifecycleRunner.java | 14 ++ 3 files changed, 155 insertions(+), 63 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/RolloverStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/RolloverStep.java index 90b9d15f21b85..542ac156a9871 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/RolloverStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/RolloverStep.java @@ -30,6 +30,11 @@ public RolloverStep(StepKey key, StepKey nextStepKey, Client client) { super(key, nextStepKey, client); } + @Override + public boolean isRetryable() { + return true; + } + @Override public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState, ClusterStateObserver observer, Listener listener) { diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index e872de5b08f4e..7cb8d92789169 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -948,69 +948,142 @@ public void testILMRolloverRetriesOnReadOnlyBlock() throws Exception { assertBusy(() -> assertThat(getStepKeyForIndex(firstIndex), equalTo(TerminalPolicyStep.KEY))); } - public void testILMRolloverOnManuallyRolledIndex() throws Exception { - String originalIndex = index + "-000001"; - String secondIndex = index + "-000002"; - String thirdIndex = index + "-000003"; - - // Set up a policy with rollover - createNewSingletonPolicy("hot", new RolloverAction(null, null, 2L)); - Request createIndexTemplate = new Request("PUT", "_template/rolling_indexes"); - createIndexTemplate.setJsonEntity("{" + - "\"index_patterns\": [\""+ index + "-*\"], \n" + - " \"settings\": {\n" + - " \"number_of_shards\": 1,\n" + - " \"number_of_replicas\": 0,\n" + - " \"index.lifecycle.name\": \"" + policy+ "\", \n" + - " \"index.lifecycle.rollover_alias\": \"alias\"\n" + - " }\n" + - "}"); - client().performRequest(createIndexTemplate); - - createIndexWithSettings( - originalIndex, - Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) - .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0), - true - ); - - // Index a document - index(client(), originalIndex, "1", "foo", "bar"); - Request refreshOriginalIndex = new Request("POST", "/" + originalIndex + "/_refresh"); - client().performRequest(refreshOriginalIndex); - - // Manual rollover - Request rolloverRequest = new Request("POST", "/alias/_rollover"); - rolloverRequest.setJsonEntity("{\n" + - " \"conditions\": {\n" + - " \"max_docs\": \"1\"\n" + - " }\n" + - "}" - ); - client().performRequest(rolloverRequest); - assertBusy(() -> assertTrue(indexExists(secondIndex))); - - // Index another document into the original index so the ILM rollover policy condition is met - index(client(), originalIndex, "2", "foo", "bar"); - client().performRequest(refreshOriginalIndex); - - // Wait for the rollover policy to execute - assertBusy(() -> assertThat(getStepKeyForIndex(originalIndex), equalTo(TerminalPolicyStep.KEY))); - - // ILM should manage the second index after attempting (and skipping) rolling the original index - assertBusy(() -> assertTrue((boolean) explainIndex(secondIndex).getOrDefault("managed", true))); - - // index some documents to trigger an ILM rollover - index(client(), "alias", "1", "foo", "bar"); - index(client(), "alias", "2", "foo", "bar"); - index(client(), "alias", "3", "foo", "bar"); - Request refreshSecondIndex = new Request("POST", "/" + secondIndex + "/_refresh"); - client().performRequest(refreshSecondIndex).getStatusLine(); - - // ILM should rollover the second index even though it skipped the first one - assertBusy(() -> assertThat(getStepKeyForIndex(secondIndex), equalTo(TerminalPolicyStep.KEY))); - assertBusy(() -> assertTrue(indexExists(thirdIndex))); - } + public void testILMRolloverOnManuallyRolledIndex() throws Exception { + String originalIndex = index + "-000001"; + String secondIndex = index + "-000002"; + String thirdIndex = index + "-000003"; + + // Set up a policy with rollover + createNewSingletonPolicy("hot", new RolloverAction(null, null, 2L)); + Request createIndexTemplate = new Request("PUT", "_template/rolling_indexes"); + createIndexTemplate.setJsonEntity("{" + + "\"index_patterns\": [\"" + index + "-*\"], \n" + + " \"settings\": {\n" + + " \"number_of_shards\": 1,\n" + + " \"number_of_replicas\": 0,\n" + + " \"index.lifecycle.name\": \"" + policy + "\", \n" + + " \"index.lifecycle.rollover_alias\": \"alias\"\n" + + " }\n" + + "}"); + client().performRequest(createIndexTemplate); + + createIndexWithSettings( + originalIndex, + Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0), + true + ); + + // Index a document + index(client(), originalIndex, "1", "foo", "bar"); + Request refreshOriginalIndex = new Request("POST", "/" + originalIndex + "/_refresh"); + client().performRequest(refreshOriginalIndex); + + // Manual rollover + Request rolloverRequest = new Request("POST", "/alias/_rollover"); + rolloverRequest.setJsonEntity("{\n" + + " \"conditions\": {\n" + + " \"max_docs\": \"1\"\n" + + " }\n" + + "}" + ); + client().performRequest(rolloverRequest); + assertBusy(() -> assertTrue(indexExists(secondIndex))); + + // Index another document into the original index so the ILM rollover policy condition is met + index(client(), originalIndex, "2", "foo", "bar"); + client().performRequest(refreshOriginalIndex); + + // Wait for the rollover policy to execute + assertBusy(() -> assertThat(getStepKeyForIndex(originalIndex), equalTo(TerminalPolicyStep.KEY))); + + // ILM should manage the second index after attempting (and skipping) rolling the original index + assertBusy(() -> assertTrue((boolean) explainIndex(secondIndex).getOrDefault("managed", true))); + + // index some documents to trigger an ILM rollover + index(client(), "alias", "1", "foo", "bar"); + index(client(), "alias", "2", "foo", "bar"); + index(client(), "alias", "3", "foo", "bar"); + Request refreshSecondIndex = new Request("POST", "/" + secondIndex + "/_refresh"); + client().performRequest(refreshSecondIndex).getStatusLine(); + + // ILM should rollover the second index even though it skipped the first one + assertBusy(() -> assertThat(getStepKeyForIndex(secondIndex), equalTo(TerminalPolicyStep.KEY))); + assertBusy(() -> assertTrue(indexExists(thirdIndex))); + } + + public void testRolloverStepRetriesUntilRolledOverIndexIsDeleted() throws Exception { + String index = this.index + "-000001"; + String rolledIndex = this.index + "-000002"; + + createNewSingletonPolicy("hot", new RolloverAction(null, TimeValue.timeValueSeconds(1), null)); + + // create the rolled index so the rollover of the first index fails + createIndexWithSettings( + rolledIndex, + Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) + .put(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS, "alias"), + false + ); + + createIndexWithSettings( + index, + Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) + .put(LifecycleSettings.LIFECYCLE_NAME, policy) + .put(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS, "alias"), + true + ); + + assertBusy(() -> assertThat((Integer) explainIndex(index).get(FAILED_STEP_RETRY_COUNT_FIELD), greaterThanOrEqualTo(1)), 30, + TimeUnit.SECONDS); + + Request moveToStepRequest = new Request("POST", "_ilm/move/" + index); + moveToStepRequest.setJsonEntity("{\n" + + " \"current_step\": {\n" + + " \"phase\": \"hot\",\n" + + " \"action\": \"rollover\",\n" + + " \"name\": \"check-rollover-ready\"\n" + + " },\n" + + " \"next_step\": {\n" + + " \"phase\": \"hot\",\n" + + " \"action\": \"rollover\",\n" + + " \"name\": \"attempt-rollover\"\n" + + " }\n" + + "}"); + + // Using {@link #waitUntil} here as ILM moves back and forth between the {@link WaitForRolloverReadyStep} step and + // {@link org.elasticsearch.xpack.core.ilm.ErrorStep} in order to retry the failing step. As {@link #assertBusy} + // increases the wait time between calls exponentially, we might miss the window where the policy is on + // {@link WaitForRolloverReadyStep} and the move to `attempt-rollover` request will not be successful. + waitUntil(() -> { + try { + return client().performRequest(moveToStepRequest).getStatusLine().getStatusCode() == 200; + } catch (IOException e) { + return false; + } + }, 30, TimeUnit.SECONDS); + + // Similar to above, using {@link #waitUntil} as we want to make sure the `attempt-rollover` step started failing and is being + // retried (which means ILM moves back and forth between the `attempt-rollover` step and the `error` step) + waitUntil(() -> { + try { + Map explainIndexResponse = explainIndex(index); + String step = (String) explainIndexResponse.get("step"); + Integer retryCount = (Integer) explainIndexResponse.get(FAILED_STEP_RETRY_COUNT_FIELD); + return step != null && step.equals("attempt-rollover") && retryCount != null && retryCount >= 1; + } catch (IOException e) { + return false; + } + }, 30, TimeUnit.SECONDS); + + deleteIndex(rolledIndex); + + // the rollover step should eventually succeed + assertBusy(() -> assertThat(indexExists(rolledIndex), is(true))); + assertBusy(() -> assertThat(getStepKeyForIndex(index), equalTo(TerminalPolicyStep.KEY))); + } public void testHistoryIsWrittenWithSuccess() throws Exception { String index = "index"; diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java index 736d5decc1123..8e892a351d655 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java @@ -203,6 +203,20 @@ public void onFailure(String source, Exception e) { logger.error(new ParameterizedMessage("retry execution of step [{}] for index [{}] failed", failedStep.getKey().getName(), index), e); } + + @Override + public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { + if (oldState.equals(newState) == false) { + IndexMetaData newIndexMeta = newState.metaData().index(index); + Step indexMetaCurrentStep = getCurrentStep(stepRegistry, policy, newIndexMeta); + StepKey stepKey = indexMetaCurrentStep.getKey(); + if (stepKey != null && stepKey != TerminalPolicyStep.KEY && newIndexMeta != null) { + logger.trace("policy [{}] for index [{}] was moved back on the failed step for as part of an automatic " + + "retry. Attempting to execute the failed step [{}] if it's an async action", policy, index, stepKey); + maybeRunAsyncAction(newState, newIndexMeta, policy, stepKey); + } + } + } }); } else { logger.debug("policy [{}] for index [{}] on an error step after a terminal error, skipping execution", policy, index); From 588d6ebee8b1ec254db742c37b05fdc001df2b2f Mon Sep 17 00:00:00 2001 From: kkewwei Date: Fri, 3 Jan 2020 19:23:20 +0800 Subject: [PATCH 366/686] Log index name when updating index settings (#49969) Today we log changes to index settings like this: updating [index.setting.blah] from [A] to [B] The identity of the index whose settings were updated is conspicuously absent from this message. This commit addresses this by adding the index name to these messages. Fixes #49818. --- .../settings/AbstractScopedSettings.java | 6 ++- .../common/settings/IndexScopedSettings.java | 3 +- .../common/settings/SettingTests.java | 41 +++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java index 81b3b92844d6b..99c651ef7df80 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java @@ -55,7 +55,7 @@ public abstract class AbstractScopedSettings { private static final Pattern GROUP_KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])+$"); private static final Pattern AFFIX_KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])+[*](?:[.][-\\w]+)+$"); - protected final Logger logger = LogManager.getLogger(this.getClass()); + private final Logger logger; private final Settings settings; private final List> settingUpdaters = new CopyOnWriteArrayList<>(); @@ -70,6 +70,7 @@ protected AbstractScopedSettings( final Set> settingsSet, final Set> settingUpgraders, final Setting.Property scope) { + this.logger = LogManager.getLogger(this.getClass()); this.settings = settings; this.lastSettingsApplied = Settings.EMPTY; @@ -110,7 +111,8 @@ protected void validateSettingKey(Setting setting) { } } - protected AbstractScopedSettings(Settings nodeSettings, Settings scopeSettings, AbstractScopedSettings other) { + protected AbstractScopedSettings(Settings nodeSettings, Settings scopeSettings, AbstractScopedSettings other, Logger logger) { + this.logger = logger; this.settings = nodeSettings; this.lastSettingsApplied = scopeSettings; this.scope = other.scope; diff --git a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java index f83012f2db32b..e278f01002595 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java @@ -24,6 +24,7 @@ import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; import org.elasticsearch.cluster.routing.allocation.decider.MaxRetryAllocationDecider; import org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.index.IndexModule; import org.elasticsearch.index.IndexSettings; @@ -186,7 +187,7 @@ public IndexScopedSettings(Settings settings, Set> settingsSet) { } private IndexScopedSettings(Settings settings, IndexScopedSettings other, IndexMetaData metaData) { - super(settings, metaData.getSettings(), other); + super(settings, metaData.getSettings(), other, Loggers.getLogger(IndexScopedSettings.class, metaData.getIndex())); } public IndexScopedSettings copy(Settings settings, IndexMetaData metaData) { diff --git a/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java b/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java index 4ad401454b914..889bfadb10ec7 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java @@ -18,14 +18,23 @@ */ package org.elasticsearch.common.settings; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LogEvent; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.AbstractScopedSettings.SettingUpdater; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.monitor.jvm.JvmInfo; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.MockLogAppender; +import org.elasticsearch.test.junit.annotations.TestLogging; import java.util.Arrays; import java.util.Collections; @@ -40,6 +49,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.elasticsearch.index.IndexSettingsTests.newIndexMeta; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -1092,4 +1102,35 @@ public void testNonSecureSettingInKeystore() { assertThat(e.getMessage(), containsString("must be stored inside elasticsearch.yml")); } + @TestLogging(value="org.elasticsearch.common.settings.IndexScopedSettings:INFO", + reason="to ensure we log INFO-level messages from IndexScopedSettings") + public void testLogSettingUpdate() throws Exception { + final IndexMetaData metaData = newIndexMeta("index1", + Settings.builder().put(IndexSettings.INDEX_REFRESH_INTERVAL_SETTING.getKey(), "20s").build()); + final IndexSettings settings = new IndexSettings(metaData, Settings.EMPTY); + + final MockLogAppender mockLogAppender = new MockLogAppender(); + mockLogAppender.addExpectation(new MockLogAppender.SeenEventExpectation( + "message", + "org.elasticsearch.common.settings.IndexScopedSettings", + Level.INFO, + "updating [index.refresh_interval] from [20s] to [10s]") { + @Override + public boolean innerMatch(LogEvent event) { + return event.getMarker().getName().equals(" [index1]"); + } + }); + mockLogAppender.start(); + final Logger logger = LogManager.getLogger(IndexScopedSettings.class); + try { + Loggers.addAppender(logger, mockLogAppender); + settings.updateIndexMetaData(newIndexMeta("index1", + Settings.builder().put(IndexSettings.INDEX_REFRESH_INTERVAL_SETTING.getKey(), "10s").build())); + + mockLogAppender.assertAllExpectationsMatched(); + } finally { + Loggers.removeAppender(logger, mockLogAppender); + mockLogAppender.stop(); + } + } } From 62b023538e558bea33dd5bb6dd6a85b269f6205e Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Fri, 3 Jan 2020 11:24:12 +0000 Subject: [PATCH 367/686] Adjust version skips for intervals rest tests (#50588) The rest tests for prefix, wildcard and fuzzy intervals can all be run against 7.x clusters. Also corrects the invocation of the fuzzy interval. --- .../test/search/230_interval_query.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/230_interval_query.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/230_interval_query.yml index 654ef2a2e173f..ea29e5b208b23 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/230_interval_query.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/230_interval_query.yml @@ -387,8 +387,8 @@ setup: --- "Test prefix": - skip: - version: " - 8.0.0" - reason: "TODO: change to 7.3 in backport" + version: " - 7.2.99" + reason: "Implemented in 7.3" - do: search: index: test @@ -407,8 +407,8 @@ setup: --- "Test wildcard": - skip: - version: " - 8.0.0" - reason: "TODO: change to 7.3 in backport" + version: " - 7.2.99" + reason: "Implemented in 7.3" - do: search: index: test @@ -427,8 +427,8 @@ setup: --- "Test fuzzy match": - skip: - version: " - 8.0.0" - reason: "TODO: change to 7.6 in backport" + version: " - 7.5.99" + reason: "Implemented in 7.6" - do: search: index: test @@ -439,7 +439,7 @@ setup: all_of: intervals: - fuzzy: - query: cald + term: cald - prefix: prefix: out - match: { hits.total.value: 3 } From a1b9bbb0e90d17b41914243caef4e870331f4acc Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 3 Jan 2020 13:25:28 +0100 Subject: [PATCH 368/686] Remove some Dead Code from Discovery Plugins (#50592) None of this stuff is used. --- .../classic/AzureServiceDisableException.java | 3 -- .../classic/AzureServiceRemoteException.java | 3 -- .../discovery/ec2/AwsEc2ServiceImpl.java | 4 +- .../discovery/ec2/AwsEc2ServiceImplTests.java | 4 +- .../elasticsearch/cloud/gce/GceModule.java | 47 ------------------- ...ingClusterStatePublishResponseHandler.java | 2 +- .../discovery/DiscoveryModule.java | 3 +- .../MasterNotDiscoveredException.java | 5 -- .../gateway/LocalAllocateDangledIndices.java | 2 +- .../java/org/elasticsearch/node/Node.java | 2 +- ...usterStatePublishResponseHandlerTests.java | 2 +- .../discovery/DiscoveryModuleTests.java | 9 +--- .../ml/action/TransportCloseJobAction.java | 2 +- ...TransportStopDataFrameAnalyticsAction.java | 2 +- .../action/TransportStopDatafeedAction.java | 2 +- .../TransportDeleteRollupJobAction.java | 2 +- .../action/TransportGetRollupJobAction.java | 2 +- .../action/TransportStopTransformAction.java | 2 +- 18 files changed, 17 insertions(+), 81 deletions(-) delete mode 100644 plugins/discovery-gce/src/main/java/org/elasticsearch/cloud/gce/GceModule.java diff --git a/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/cloud/azure/classic/AzureServiceDisableException.java b/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/cloud/azure/classic/AzureServiceDisableException.java index 66488f90c31bf..0f2e255116645 100644 --- a/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/cloud/azure/classic/AzureServiceDisableException.java +++ b/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/cloud/azure/classic/AzureServiceDisableException.java @@ -20,9 +20,6 @@ package org.elasticsearch.cloud.azure.classic; public class AzureServiceDisableException extends IllegalStateException { - public AzureServiceDisableException(String msg) { - super(msg); - } public AzureServiceDisableException(String msg, Throwable cause) { super(msg, cause); diff --git a/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/cloud/azure/classic/AzureServiceRemoteException.java b/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/cloud/azure/classic/AzureServiceRemoteException.java index c961c03ba719c..e61b39a03bb29 100644 --- a/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/cloud/azure/classic/AzureServiceRemoteException.java +++ b/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/cloud/azure/classic/AzureServiceRemoteException.java @@ -20,9 +20,6 @@ package org.elasticsearch.cloud.azure.classic; public class AzureServiceRemoteException extends IllegalStateException { - public AzureServiceRemoteException(String msg) { - super(msg); - } public AzureServiceRemoteException(String msg, Throwable cause) { super(msg, cause); diff --git a/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImpl.java b/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImpl.java index 546634c88cf45..92b5f740dc127 100644 --- a/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImpl.java +++ b/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImpl.java @@ -44,7 +44,7 @@ class AwsEc2ServiceImpl implements AwsEc2Service { private AmazonEC2 buildClient(Ec2ClientSettings clientSettings) { final AWSCredentialsProvider credentials = buildCredentials(logger, clientSettings); - final ClientConfiguration configuration = buildConfiguration(logger, clientSettings); + final ClientConfiguration configuration = buildConfiguration(clientSettings); final AmazonEC2 client = buildClient(credentials, configuration); if (Strings.hasText(clientSettings.endpoint)) { logger.debug("using explicit ec2 endpoint [{}]", clientSettings.endpoint); @@ -60,7 +60,7 @@ AmazonEC2 buildClient(AWSCredentialsProvider credentials, ClientConfiguration co } // pkg private for tests - static ClientConfiguration buildConfiguration(Logger logger, Ec2ClientSettings clientSettings) { + static ClientConfiguration buildConfiguration(Ec2ClientSettings clientSettings) { final ClientConfiguration clientConfiguration = new ClientConfiguration(); // the response metadata cache is only there for diagnostics purposes, // but can force objects from every response to the old generation. diff --git a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImplTests.java b/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImplTests.java index 148e58d7b3c06..931cc891a91fc 100644 --- a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImplTests.java +++ b/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/AwsEc2ServiceImplTests.java @@ -120,8 +120,8 @@ protected void launchAWSConfigurationTest(Settings settings, String expectedProxyUsername, String expectedProxyPassword, int expectedReadTimeout) { - final ClientConfiguration configuration = AwsEc2ServiceImpl.buildConfiguration(logger, - Ec2ClientSettings.getClientSettings(settings)); + final ClientConfiguration configuration = AwsEc2ServiceImpl.buildConfiguration( + Ec2ClientSettings.getClientSettings(settings)); assertThat(configuration.getResponseMetadataCacheSize(), is(0)); assertThat(configuration.getProtocol(), is(expectedProtocol)); diff --git a/plugins/discovery-gce/src/main/java/org/elasticsearch/cloud/gce/GceModule.java b/plugins/discovery-gce/src/main/java/org/elasticsearch/cloud/gce/GceModule.java deleted file mode 100644 index 064fe606244ee..0000000000000 --- a/plugins/discovery-gce/src/main/java/org/elasticsearch/cloud/gce/GceModule.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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. - */ - -package org.elasticsearch.cloud.gce; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.common.inject.AbstractModule; -import org.elasticsearch.common.settings.Settings; - -public class GceModule extends AbstractModule { - // pkg private so tests can override with mock - static Class computeServiceImpl = GceInstancesServiceImpl.class; - - protected final Settings settings; - protected final Logger logger = LogManager.getLogger(GceModule.class); - - public GceModule(Settings settings) { - this.settings = settings; - } - - public static Class getComputeServiceImpl() { - return computeServiceImpl; - } - - @Override - protected void configure() { - logger.debug("configure GceModule (bind compute service)"); - bind(GceInstancesService.class).to(computeServiceImpl).asEagerSingleton(); - } -} diff --git a/server/src/main/java/org/elasticsearch/discovery/BlockingClusterStatePublishResponseHandler.java b/server/src/main/java/org/elasticsearch/discovery/BlockingClusterStatePublishResponseHandler.java index 8d5ef1926cdd5..5aac5758fc467 100644 --- a/server/src/main/java/org/elasticsearch/discovery/BlockingClusterStatePublishResponseHandler.java +++ b/server/src/main/java/org/elasticsearch/discovery/BlockingClusterStatePublishResponseHandler.java @@ -64,7 +64,7 @@ public void onResponse(DiscoveryNode node) { * Called for each failure obtained from non master nodes * @param node the node that replied to the publish event */ - public void onFailure(DiscoveryNode node, Exception e) { + public void onFailure(DiscoveryNode node) { boolean found = pendingNodes.remove(node); assert found : "node [" + node + "] already responded or failed"; boolean added = failedNodes.add(node); diff --git a/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java b/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java index d1b008ec76eb7..843562ef9e619 100644 --- a/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java +++ b/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java @@ -39,7 +39,6 @@ import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.gateway.GatewayMetaState; import org.elasticsearch.plugins.DiscoveryPlugin; -import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import java.nio.file.Path; @@ -82,7 +81,7 @@ public class DiscoveryModule { private final Discovery discovery; - public DiscoveryModule(Settings settings, ThreadPool threadPool, TransportService transportService, + public DiscoveryModule(Settings settings, TransportService transportService, NamedWriteableRegistry namedWriteableRegistry, NetworkService networkService, MasterService masterService, ClusterApplier clusterApplier, ClusterSettings clusterSettings, List plugins, AllocationService allocationService, Path configFile, GatewayMetaState gatewayMetaState, diff --git a/server/src/main/java/org/elasticsearch/discovery/MasterNotDiscoveredException.java b/server/src/main/java/org/elasticsearch/discovery/MasterNotDiscoveredException.java index 282d849debd44..e79a8f7b0f202 100644 --- a/server/src/main/java/org/elasticsearch/discovery/MasterNotDiscoveredException.java +++ b/server/src/main/java/org/elasticsearch/discovery/MasterNotDiscoveredException.java @@ -30,15 +30,10 @@ public class MasterNotDiscoveredException extends ElasticsearchException { public MasterNotDiscoveredException() { super(""); } - public MasterNotDiscoveredException(Throwable cause) { super(cause); } - public MasterNotDiscoveredException(String message) { - super(message); - } - @Override public RestStatus status() { return RestStatus.SERVICE_UNAVAILABLE; diff --git a/server/src/main/java/org/elasticsearch/gateway/LocalAllocateDangledIndices.java b/server/src/main/java/org/elasticsearch/gateway/LocalAllocateDangledIndices.java index 281c96415457d..a90e31ada1cf2 100644 --- a/server/src/main/java/org/elasticsearch/gateway/LocalAllocateDangledIndices.java +++ b/server/src/main/java/org/elasticsearch/gateway/LocalAllocateDangledIndices.java @@ -80,7 +80,7 @@ public void allocateDangled(Collection indices, ActionListener> getSeedHostProviders(TransportS @Before public void setupDummyServices() { - threadPool = mock(ThreadPool.class); - when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); - transportService = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null); + transportService = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, mock(ThreadPool.class), null); masterService = mock(MasterService.class); namedWriteableRegistry = new NamedWriteableRegistry(Collections.emptyList()); clusterApplier = mock(ClusterApplier.class); @@ -90,7 +85,7 @@ public void clearDummyServices() throws IOException { } private DiscoveryModule newModule(Settings settings, List plugins) { - return new DiscoveryModule(settings, threadPool, transportService, namedWriteableRegistry, null, masterService, + return new DiscoveryModule(settings, transportService, namedWriteableRegistry, null, masterService, clusterApplier, clusterSettings, plugins, null, createTempDir().toAbsolutePath(), gatewayMetaState, mock(RerouteService.class)); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java index c79ee507bd9d2..c0131cb5a8099 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java @@ -87,7 +87,7 @@ protected void doExecute(Task task, CloseJobAction.Request request, ActionListen // Delegates close job to elected master node, so it becomes the coordinating node. // See comment in OpenJobAction.Transport class for more information. if (nodes.getMasterNode() == null) { - listener.onFailure(new MasterNotDiscoveredException("no known master node")); + listener.onFailure(new MasterNotDiscoveredException()); } else { transportService.sendRequest(nodes.getMasterNode(), actionName, request, new ActionListenerResponseHandler<>(listener, CloseJobAction.Response::new)); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java index 2d0f3adfd14ab..881c6e410f743 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java @@ -202,7 +202,7 @@ private String[] findAllocatedNodesAndRemoveUnassignedTasks(Set analytic private void redirectToMasterNode(DiscoveryNode masterNode, StopDataFrameAnalyticsAction.Request request, ActionListener listener) { if (masterNode == null) { - listener.onFailure(new MasterNotDiscoveredException("no known master node")); + listener.onFailure(new MasterNotDiscoveredException()); } else { transportService.sendRequest(masterNode, actionName, request, new ActionListenerResponseHandler<>(listener, StopDataFrameAnalyticsAction.Response::new)); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java index d90f175e46b1f..d254aeebd5b4b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java @@ -125,7 +125,7 @@ protected void doExecute(Task task, StopDatafeedAction.Request request, ActionLi // Delegates stop datafeed to elected master node, so it becomes the coordinating node. // See comment in TransportStartDatafeedAction for more information. if (nodes.getMasterNode() == null) { - listener.onFailure(new MasterNotDiscoveredException("no known master node")); + listener.onFailure(new MasterNotDiscoveredException()); } else { transportService.sendRequest(nodes.getMasterNode(), actionName, request, new ActionListenerResponseHandler<>(listener, StopDatafeedAction.Response::new)); diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportDeleteRollupJobAction.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportDeleteRollupJobAction.java index f5a484a8e2751..ac37ee7288ae9 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportDeleteRollupJobAction.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportDeleteRollupJobAction.java @@ -57,7 +57,7 @@ protected void doExecute(Task task, DeleteRollupJobAction.Request request, Actio // Non-master nodes may have a stale cluster state that shows jobs which are cancelled // on the master, which makes testing difficult. if (nodes.getMasterNode() == null) { - listener.onFailure(new MasterNotDiscoveredException("no known master nodes")); + listener.onFailure(new MasterNotDiscoveredException()); } else { transportService.sendRequest(nodes.getMasterNode(), actionName, request, new ActionListenerResponseHandler<>(listener, DeleteRollupJobAction.Response::new)); diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportGetRollupJobAction.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportGetRollupJobAction.java index 836ccfd2cfef7..ec7f5495aa314 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportGetRollupJobAction.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportGetRollupJobAction.java @@ -59,7 +59,7 @@ protected void doExecute(Task task, GetRollupJobsAction.Request request, ActionL // Non-master nodes may have a stale cluster state that shows jobs which are cancelled // on the master, which makes testing difficult. if (nodes.getMasterNode() == null) { - listener.onFailure(new MasterNotDiscoveredException("no known master nodes")); + listener.onFailure(new MasterNotDiscoveredException()); } else { transportService.sendRequest(nodes.getMasterNode(), actionName, request, new ActionListenerResponseHandler<>(listener, GetRollupJobsAction.Response::new)); diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java index aaf51396abb87..bd3ddc3e8ceab 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java @@ -134,7 +134,7 @@ protected void doExecute(Task task, Request request, ActionListener li if (nodes.isLocalNodeElectedMaster() == false) { // Delegates stop transform to elected master node so it becomes the coordinating node. if (nodes.getMasterNode() == null) { - listener.onFailure(new MasterNotDiscoveredException("no known master node")); + listener.onFailure(new MasterNotDiscoveredException()); } else { transportService.sendRequest( nodes.getMasterNode(), From 7e6a793c62f5115f52090e716f32937a50ee2a68 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Fri, 3 Jan 2020 08:34:11 -0500 Subject: [PATCH 369/686] [DOCS] Reformat uppercase token filter docs (#50555) * Updates the description and adds a Lucene link * Adds analyze and custom analyzer snippets --- .../uppercase-tokenfilter.asciidoc | 101 +++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/docs/reference/analysis/tokenfilters/uppercase-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/uppercase-tokenfilter.asciidoc index c745f247ec3d9..06ea2c3279c56 100644 --- a/docs/reference/analysis/tokenfilters/uppercase-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/uppercase-tokenfilter.asciidoc @@ -4,5 +4,102 @@ Uppercase ++++ -A token filter of type `uppercase` that normalizes token text to upper -case. +Changes token text to uppercase. For example, you can use the `uppercase` filter +to change `the Lazy DoG` to `THE LAZY DOG`. + +This filter uses Lucene's +https://lucene.apache.org/core/{lucene_version_path}/analyzers-common/org/apache/lucene/analysis/core/UpperCaseFilter.html[UpperCaseFilter]. + +[WARNING] +==== +Depending on the language, an uppercase character can map to multiple +lowercase characters. Using the `uppercase` filter could result in the loss of +lowercase character information. + +To avoid this loss but still have a consistent lettercase, use the <> filter instead. +==== + +[[analysis-uppercase-tokenfilter-analyze-ex]] +==== Example + +The following <> request uses the default +`uppercase` filter to change the `the Quick FoX JUMPs` to uppercase: + +[source,console] +-------------------------------------------------- +GET _analyze +{ + "tokenizer" : "standard", + "filter" : ["uppercase"], + "text" : "the Quick FoX JUMPs" +} +-------------------------------------------------- + +The filter produces the following tokens: + +[source,text] +-------------------------------------------------- +[ THE, QUICK, FOX, JUMPS ] +-------------------------------------------------- + +///////////////////// +[source,console-result] +-------------------------------------------------- +{ + "tokens" : [ + { + "token" : "THE", + "start_offset" : 0, + "end_offset" : 3, + "type" : "", + "position" : 0 + }, + { + "token" : "QUICK", + "start_offset" : 4, + "end_offset" : 9, + "type" : "", + "position" : 1 + }, + { + "token" : "FOX", + "start_offset" : 10, + "end_offset" : 13, + "type" : "", + "position" : 2 + }, + { + "token" : "JUMPS", + "start_offset" : 14, + "end_offset" : 19, + "type" : "", + "position" : 3 + } + ] +} +-------------------------------------------------- +///////////////////// + +[[analysis-uppercase-tokenfilter-analyzer-ex]] +==== Add to an analyzer + +The following <> request uses the +`uppercase` filter to configure a new +<>. + +[source,console] +-------------------------------------------------- +PUT uppercase_example +{ + "settings" : { + "analysis" : { + "analyzer" : { + "whitespace_uppercase" : { + "tokenizer" : "whitespace", + "filter" : ["uppercase"] + } + } + } + } +} +-------------------------------------------------- From 46686a7fefd87335b38c0c9f4f7bffb643e68e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Fri, 3 Jan 2020 14:49:43 +0100 Subject: [PATCH 370/686] [DOCS] Fine-tunes training_percent definition. (#50601) --- docs/reference/ml/ml-shared.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ml/ml-shared.asciidoc b/docs/reference/ml/ml-shared.asciidoc index 7b0f8b64718a7..61e862486f12a 100644 --- a/docs/reference/ml/ml-shared.asciidoc +++ b/docs/reference/ml/ml-shared.asciidoc @@ -1064,8 +1064,8 @@ end::tokenizer[] tag::training-percent[] Defines what percentage of the eligible documents that will be used for training. Documents that are ignored by the analysis (for example -those that contain arrays) won’t be included in the calculation for used -percentage. Defaults to `100`. +those that contain arrays with more than one value) won’t be included in the +calculation for used percentage. Defaults to `100`. end::training-percent[] tag::use-null[] From 9e178689b7558c864464a3fb2c9f35c995cf3b86 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 3 Jan 2020 16:44:38 +0200 Subject: [PATCH 371/686] [ML] Unmute BWC tests and fix version after backport of #50553 (#50599) Relates #50553 --- .../DeleteDataFrameAnalyticsAction.java | 4 +-- .../90_ml_data_frame_analytics_crud.yml | 28 +++++++++++++++---- .../90_ml_data_frame_analytics_crud.yml | 3 -- .../90_ml_data_frame_analytics_crud.yml | 5 ---- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDataFrameAnalyticsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDataFrameAnalyticsAction.java index 40d70d979be9e..74a473ec6832b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDataFrameAnalyticsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDataFrameAnalyticsAction.java @@ -40,7 +40,7 @@ public static class Request extends AcknowledgedRequest { public Request(StreamInput in) throws IOException { super(in); id = in.readString(); - if (in.getVersion().onOrAfter(Version.CURRENT)) { + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { force = in.readBoolean(); } else { force = false; @@ -82,7 +82,7 @@ public boolean equals(Object o) { public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeString(id); - if (out.getVersion().onOrAfter(Version.CURRENT)) { + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { out.writeBoolean(force); } } diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml index a6fbc5cc4e2c0..af304afc57db6 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,8 +1,3 @@ -setup: - - skip: - version: "all" - reason: "Until backport of https://github.com/elastic/elasticsearch/issues/50553" - --- "Get old outlier_detection job": @@ -148,3 +143,26 @@ setup: - match: { count: 1 } - match: { data_frame_analytics.0.id: "mixed_cluster_outlier_detection_job" } - match: { data_frame_analytics.0.state: "stopped" } + +--- +"Put and delete a job": + + - do: + ml.put_data_frame_analytics: + id: "mixed_cluster_job_to_delete" + body: > + { + "source": { + "index": "bwc_ml_outlier_detection_job_source" + }, + "dest": { + "index": "mixed_cluster_job_to_delete_results" + }, + "analysis": {"outlier_detection":{}} + } + - match: { id: "mixed_cluster_job_to_delete" } + + - do: + ml.delete_data_frame_analytics: + id: mixed_cluster_job_to_delete + - match: { acknowledged: true } diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml index 51f21afa6761d..fe160bba15f23 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,7 +1,4 @@ setup: - - skip: - version: "all" - reason: "Until backport of https://github.com/elastic/elasticsearch/issues/50553" - do: index: diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml index b20c63f69d8e2..14438883f0da1 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/90_ml_data_frame_analytics_crud.yml @@ -1,8 +1,3 @@ -setup: - - skip: - version: "all" - reason: "Until backport of https://github.com/elastic/elasticsearch/issues/50553" - --- "Get old cluster outlier_detection job": From 149a4c3b89f3f516a63afe2ca3849c272c15ecb0 Mon Sep 17 00:00:00 2001 From: Aleksandr Maus Date: Fri, 3 Jan 2020 10:03:48 -0500 Subject: [PATCH 372/686] Fix RestSqlQueryAction comment typos (#50603) --- .../elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java index e4807389e33a5..d389a5ba1e17d 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java @@ -49,15 +49,15 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli * {@link XContent} outputs we can't use {@link RestToXContentListener} * like everything else. We want to stick as closely as possible to * Elasticsearch's defaults though, while still layering in ways to - * control the output more easilly. + * control the output more easily. * * First we find the string that the user used to specify the response - * format. If there is a {@code format} paramter we use that. If there + * format. If there is a {@code format} parameter we use that. If there * isn't but there is a {@code Accept} header then we use that. If there * isn't then we use the {@code Content-Type} header which is required. */ String accept = null; - + if ((Mode.isDriver(sqlRequest.requestInfo().mode()) || sqlRequest.requestInfo().mode() == Mode.CLI) && (sqlRequest.binaryCommunication() == null || sqlRequest.binaryCommunication() == true)) { // enforce CBOR response for drivers and CLI (unless instructed differently through the config param) From 160eea78cf7e08f54c28284538c7c6c3a500a1ad Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Fri, 3 Jan 2020 17:14:33 +0200 Subject: [PATCH 373/686] Guard against null geoBoundingBox (#50506) A geo box with a top value of Double.NEGATIVE_INFINITY will yield an empty xContent which translates to a null `geoBoundingBox`. This commit marks the field as `Nullable` and guards against null when retrieving the `topLeft` and `bottomRight` fields. Fixes https://github.com/elastic/elasticsearch/issues/50505 --- .../search/aggregations/metrics/ParsedGeoBounds.java | 12 +++++++++--- .../aggregations/metrics/InternalGeoBoundsTests.java | 2 -- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java index 2b29ae15119d9..cbe1494b13f9d 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.metrics; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.GeoPoint; @@ -30,14 +31,17 @@ import java.io.IOException; -import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.common.geo.GeoBoundingBox.BOTTOM_RIGHT_FIELD; import static org.elasticsearch.common.geo.GeoBoundingBox.BOUNDS_FIELD; import static org.elasticsearch.common.geo.GeoBoundingBox.LAT_FIELD; import static org.elasticsearch.common.geo.GeoBoundingBox.LON_FIELD; import static org.elasticsearch.common.geo.GeoBoundingBox.TOP_LEFT_FIELD; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; public class ParsedGeoBounds extends ParsedAggregation implements GeoBounds { + + // A top of Double.NEGATIVE_INFINITY yields an empty xContent, so the bounding box is null + @Nullable private GeoBoundingBox geoBoundingBox; @Override @@ -54,13 +58,15 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th } @Override + @Nullable public GeoPoint topLeft() { - return geoBoundingBox.topLeft(); + return geoBoundingBox != null ? geoBoundingBox.topLeft() : null; } @Override + @Nullable public GeoPoint bottomRight() { - return geoBoundingBox.bottomRight(); + return geoBoundingBox != null ? geoBoundingBox.bottomRight() : null; } private static final ObjectParser PARSER = new ObjectParser<>(ParsedGeoBounds.class.getSimpleName(), true, diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBoundsTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBoundsTests.java index aa2e527b2e605..27ef688a8666d 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBoundsTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBoundsTests.java @@ -21,8 +21,6 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.search.aggregations.ParsedAggregation; -import org.elasticsearch.search.aggregations.metrics.InternalGeoBounds; -import org.elasticsearch.search.aggregations.metrics.ParsedGeoBounds; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.elasticsearch.test.InternalAggregationTestCase; From c439cbc3b0b33f0579785119c9917c6d18cd3868 Mon Sep 17 00:00:00 2001 From: Orhan Toy Date: Fri, 3 Jan 2020 16:15:18 +0100 Subject: [PATCH 374/686] [DOCS] Fix missing quote in script-score-query.asciidoc (#50590) --- docs/reference/query-dsl/script-score-query.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/query-dsl/script-score-query.asciidoc b/docs/reference/query-dsl/script-score-query.asciidoc index c74a50087170f..029cc7469e0f3 100644 --- a/docs/reference/query-dsl/script-score-query.asciidoc +++ b/docs/reference/query-dsl/script-score-query.asciidoc @@ -281,7 +281,7 @@ as described in <>. -------------------------------------------------- "script" : { "source" : "Math.log10(doc['field'].value * params.factor)", - params" : { + "params" : { "factor" : 5 } } @@ -297,7 +297,7 @@ a value `1` if a document doesn't have a field `field`: -------------------------------------------------- "script" : { "source" : "Math.log10((doc['field'].size() == 0 ? 1 : doc['field'].value()) * params.factor)", - params" : { + "params" : { "factor" : 5 } } From 0ac7475a3a3b939af7926e8914b04bff628cbeef Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Fri, 3 Jan 2020 10:47:51 -0500 Subject: [PATCH 375/686] Declare remaining parsers `final` (#50571) We have about 800 `ObjectParsers` in Elasticsearch, about 700 of which are final. This is *probably* the right way to declare them because in practice we never mutate them after they are built. And we certainly don't change the static reference. Anyway, this adds `final` to these parsers. I found the non-final parsers with this: ``` diff \ <(find . -type f -name '*.java' -exec grep -iHe 'static.*PARSER\s*=' {} \+ | sort) \ <(find . -type f -name '*.java' -exec grep -iHe 'static.*final.*PARSER\s*=' {} \+ | sort) \ 2>&1 | grep '^<' ``` --- .../org/elasticsearch/client/SyncedFlushResponse.java | 2 +- .../org/elasticsearch/client/core/MainResponse.java | 3 +-- .../client/core/MultiTermVectorsResponse.java | 2 +- .../elasticsearch/client/core/TermVectorsResponse.java | 10 +++++----- .../elasticsearch/client/ml/StartDatafeedRequest.java | 2 +- .../client/ml/dataframe/DataFrameAnalyticsConfig.java | 2 +- .../client/ml/dataframe/DataFrameAnalyticsDest.java | 2 +- .../client/ml/dataframe/DataFrameAnalyticsSource.java | 2 +- .../client/ml/dataframe/OutlierDetection.java | 2 +- .../elasticsearch/client/watcher/AckWatchResponse.java | 2 +- .../client/watcher/ActivateWatchResponse.java | 2 +- .../elasticsearch/client/watcher/GetWatchResponse.java | 2 +- .../client/watcher/WatcherStatsResponse.java | 2 +- .../smoketest/DocsClientYamlTestSuiteIT.java | 2 +- .../aggregations/matrix/stats/ParsedMatrixStats.java | 2 +- .../index/query/RankFeatureQueryBuilder.java | 2 +- .../allocation/ClusterAllocationExplainRequest.java | 2 +- .../action/admin/indices/rollover/RolloverInfo.java | 2 +- .../admin/indices/validate/query/QueryExplanation.java | 2 +- .../indices/validate/query/ValidateQueryResponse.java | 2 +- .../elasticsearch/action/bulk/BulkItemResponse.java | 2 +- .../action/fieldcaps/FieldCapabilities.java | 2 +- .../action/fieldcaps/FieldCapabilitiesRequest.java | 2 +- .../java/org/elasticsearch/common/geo/GeoJson.java | 2 +- .../org/elasticsearch/index/query/IdsQueryBuilder.java | 3 +-- .../query/functionscore/ScriptScoreQueryBuilder.java | 2 +- .../elasticsearch/index/reindex/BulkByScrollTask.java | 2 +- .../org/elasticsearch/index/seqno/RetentionLease.java | 2 +- .../org/elasticsearch/index/seqno/RetentionLeases.java | 2 +- .../org/elasticsearch/script/ScriptContextInfo.java | 6 +++--- .../org/elasticsearch/script/ScriptLanguagesInfo.java | 4 ++-- .../main/java/org/elasticsearch/search/SearchHit.java | 2 +- .../bucket/adjacency/ParsedAdjacencyMatrix.java | 2 +- .../aggregations/bucket/composite/ParsedComposite.java | 2 +- .../aggregations/bucket/filter/ParsedFilters.java | 2 +- .../aggregations/bucket/geogrid/ParsedGeoHashGrid.java | 2 +- .../aggregations/bucket/geogrid/ParsedGeoTileGrid.java | 2 +- .../histogram/DateHistogramAggregationBuilder.java | 2 +- .../bucket/histogram/ParsedAutoDateHistogram.java | 2 +- .../bucket/histogram/ParsedDateHistogram.java | 2 +- .../aggregations/bucket/histogram/ParsedHistogram.java | 2 +- .../aggregations/bucket/range/ParsedBinaryRange.java | 2 +- .../aggregations/bucket/range/ParsedDateRange.java | 2 +- .../aggregations/bucket/range/ParsedGeoDistance.java | 2 +- .../search/aggregations/bucket/range/ParsedRange.java | 2 +- .../bucket/significant/ParsedSignificantLongTerms.java | 2 +- .../significant/ParsedSignificantStringTerms.java | 2 +- .../aggregations/bucket/terms/ParsedDoubleTerms.java | 2 +- .../aggregations/bucket/terms/ParsedLongTerms.java | 2 +- .../aggregations/bucket/terms/ParsedStringTerms.java | 2 +- .../aggregations/metrics/ParsedHDRPercentileRanks.java | 2 +- .../aggregations/metrics/ParsedHDRPercentiles.java | 2 +- .../metrics/ParsedTDigestPercentileRanks.java | 2 +- .../aggregations/metrics/ParsedTDigestPercentiles.java | 2 +- .../search/aggregations/metrics/ParsedTopHits.java | 2 +- .../aggregations/pipeline/ParsedPercentilesBucket.java | 2 +- .../elasticsearch/search/sort/FieldSortBuilder.java | 2 +- .../elasticsearch/search/sort/ScoreSortBuilder.java | 2 +- .../elasticsearch/search/sort/ScriptSortBuilder.java | 2 +- .../suggest/completion/CompletionSuggestion.java | 5 ++--- .../completion/context/CategoryQueryContext.java | 2 +- .../suggest/completion/context/GeoQueryContext.java | 2 +- .../search/suggest/phrase/PhraseSuggestion.java | 3 +-- .../search/suggest/term/TermSuggestion.java | 3 +-- .../common/geo/GeoJsonSerializationTests.java | 2 +- 65 files changed, 73 insertions(+), 78 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SyncedFlushResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SyncedFlushResponse.java index 303e8328d08e8..41e9c3d062b0a 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SyncedFlushResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SyncedFlushResponse.java @@ -282,7 +282,7 @@ public static final class ShardFailure implements ToXContentFragment { private Map routing; @SuppressWarnings("unchecked") - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "shardfailure", a -> new ShardFailure((Integer)a[0], (String)a[1], (Map)a[2]) ); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainResponse.java index 09fc2b49a0156..5fa0120611fa6 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainResponse.java @@ -99,8 +99,7 @@ public int hashCode() { } public static class Version { - @SuppressWarnings("unchecked") - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(Version.class.getName(), true, args -> { return new Version((String) args[0], (String) args[1], (String) args[2], (String) args[3], (String) args[4], diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsResponse.java index 0a2974a8aa166..ea58e93eb9e34 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsResponse.java @@ -36,7 +36,7 @@ public MultiTermVectorsResponse(List responses) { this.responses = responses; } - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("multi_term_vectors", true, args -> { // as the response comes from server, we are sure that args[0] will be a list of TermVectorsResponse diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsResponse.java index f53f125cbf788..14e9ff7415099 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsResponse.java @@ -48,7 +48,7 @@ public TermVectorsResponse(String index, String id, long version, boolean found, this.termVectorList = termVectorList; } - private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("term_vectors", true, + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("term_vectors", true, args -> { // as the response comes from server, we are sure that args[5] will be a list of TermVector @SuppressWarnings("unchecked") List termVectorList = (List) args[5]; @@ -145,7 +145,7 @@ public int hashCode() { public static final class TermVector { - private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("term_vector", true, + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("term_vector", true, (args, ctxFieldName) -> { // as the response comes from server, we are sure that args[1] will be a list of Term @SuppressWarnings("unchecked") List terms = (List) args[1]; @@ -218,7 +218,7 @@ public int hashCode() { // Class containing a general field statistics for the field public static final class FieldStatistics { - private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "field_statistics", true, args -> { return new FieldStatistics((long) args[0], (int) args[1], (long) args[2]); @@ -282,7 +282,7 @@ public int hashCode() { public static final class Term { - private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("token", true, + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("token", true, (args, ctxTerm) -> { // as the response comes from server, we are sure that args[4] will be a list of Token @SuppressWarnings("unchecked") List tokens = (List) args[4]; @@ -393,7 +393,7 @@ public int hashCode() { public static final class Token { - private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("token", true, + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("token", true, args -> { return new Token((Integer) args[0], (Integer) args[1], (Integer) args[2], (String) args[3]); }); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/StartDatafeedRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/StartDatafeedRequest.java index 68e93141b01e2..f246f5b976b86 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/StartDatafeedRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/StartDatafeedRequest.java @@ -38,7 +38,7 @@ public class StartDatafeedRequest implements Validatable, ToXContentObject { public static final ParseField END = new ParseField("end"); public static final ParseField TIMEOUT = new ParseField("timeout"); - public static ConstructingObjectParser PARSER = + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("start_datafeed_request", a -> new StartDatafeedRequest((String)a[0])); static { diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsConfig.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsConfig.java index 0c3f98b9a46ad..5e803f714cd22 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsConfig.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsConfig.java @@ -58,7 +58,7 @@ public static Builder builder() { private static final ParseField VERSION = new ParseField("version"); private static final ParseField ALLOW_LAZY_START = new ParseField("allow_lazy_start"); - private static ObjectParser PARSER = new ObjectParser<>("data_frame_analytics_config", true, Builder::new); + private static final ObjectParser PARSER = new ObjectParser<>("data_frame_analytics_config", true, Builder::new); static { PARSER.declareString(Builder::setId, ID); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsDest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsDest.java index 4123f85ee2f43..3b765c9b49c69 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsDest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsDest.java @@ -45,7 +45,7 @@ public static Builder builder() { private static final ParseField INDEX = new ParseField("index"); private static final ParseField RESULTS_FIELD = new ParseField("results_field"); - private static ObjectParser PARSER = new ObjectParser<>("data_frame_analytics_dest", true, Builder::new); + private static final ObjectParser PARSER = new ObjectParser<>("data_frame_analytics_dest", true, Builder::new); static { PARSER.declareString(Builder::setIndex, INDEX); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSource.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSource.java index 1f731f4c28aaa..29672fb569d6e 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSource.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSource.java @@ -47,7 +47,7 @@ public static Builder builder() { private static final ParseField QUERY = new ParseField("query"); public static final ParseField _SOURCE = new ParseField("_source"); - private static ObjectParser PARSER = new ObjectParser<>("data_frame_analytics_source", true, Builder::new); + private static final ObjectParser PARSER = new ObjectParser<>("data_frame_analytics_source", true, Builder::new); static { PARSER.declareStringArray(Builder::setIndex, INDEX); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/OutlierDetection.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/OutlierDetection.java index f58c9b3d35429..ceb6b6e71f2ee 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/OutlierDetection.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/OutlierDetection.java @@ -51,7 +51,7 @@ public static Builder builder() { static final ParseField OUTLIER_FRACTION = new ParseField("outlier_fraction"); static final ParseField STANDARDIZATION_ENABLED = new ParseField("standardization_enabled"); - private static ObjectParser PARSER = new ObjectParser<>(NAME.getPreferredName(), true, Builder::new); + private static final ObjectParser PARSER = new ObjectParser<>(NAME.getPreferredName(), true, Builder::new); static { PARSER.declareInt(Builder::setNNeighbors, N_NEIGHBORS); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/AckWatchResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/AckWatchResponse.java index 5c6750193a7c0..61245462dbecf 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/AckWatchResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/AckWatchResponse.java @@ -45,7 +45,7 @@ public WatchStatus getStatus() { } private static final ParseField STATUS_FIELD = new ParseField("status"); - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("ack_watch_response", true, a -> new AckWatchResponse((WatchStatus) a[0])); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/ActivateWatchResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/ActivateWatchResponse.java index b1e63e767f3be..d4dc044df647d 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/ActivateWatchResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/ActivateWatchResponse.java @@ -32,7 +32,7 @@ public final class ActivateWatchResponse { private static final ParseField STATUS_FIELD = new ParseField("status"); - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("activate_watch_response", true, a -> new ActivateWatchResponse((WatchStatus) a[0])); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchResponse.java index 83727003106e9..43a1a2a781520 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchResponse.java @@ -132,7 +132,7 @@ public int hashCode() { private static final ParseField STATUS_FIELD = new ParseField("status"); private static final ParseField WATCH_FIELD = new ParseField("watch"); - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("get_watch_response", true, a -> { boolean isFound = (boolean) a[1]; diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatcherStatsResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatcherStatsResponse.java index 708954e666b5f..513adc8bd1478 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatcherStatsResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatcherStatsResponse.java @@ -82,7 +82,7 @@ public String getClusterName() { } @SuppressWarnings("unchecked") - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("watcher_stats_response", true, a -> new WatcherStatsResponse((NodesResponseHeader) a[0], (String) a[1], new WatcherMetaData((boolean) a[2]), (List) a[3])); diff --git a/docs/src/test/java/org/elasticsearch/smoketest/DocsClientYamlTestSuiteIT.java b/docs/src/test/java/org/elasticsearch/smoketest/DocsClientYamlTestSuiteIT.java index 14fef43344644..ff796aba81d69 100644 --- a/docs/src/test/java/org/elasticsearch/smoketest/DocsClientYamlTestSuiteIT.java +++ b/docs/src/test/java/org/elasticsearch/smoketest/DocsClientYamlTestSuiteIT.java @@ -125,7 +125,7 @@ protected boolean isTransformTest() { * small number of tokens. */ private static class CompareAnalyzers implements ExecutableSection { - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("test_analyzer", false, (a, location) -> { String index = (String) a[0]; String first = (String) a[1]; diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java index ba28e0b0ee361..660ecc930982c 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java +++ b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java @@ -187,7 +187,7 @@ static class ParsedMatrixStatsResult { Map covariances; Map correlations; - private static ObjectParser RESULT_PARSER = + private static final ObjectParser RESULT_PARSER = new ObjectParser<>(ParsedMatrixStatsResult.class.getSimpleName(), true, ParsedMatrixStatsResult::new); static { RESULT_PARSER.declareString((result, name) -> result.name = name, diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/query/RankFeatureQueryBuilder.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/query/RankFeatureQueryBuilder.java index c934e0c1f79db..86990d95c42c6 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/query/RankFeatureQueryBuilder.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/query/RankFeatureQueryBuilder.java @@ -260,7 +260,7 @@ private static ScoreFunction readScoreFunction(StreamInput in) throws IOExceptio } } - public static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "feature", args -> { final String field = (String) args[0]; final float boost = args[1] == null ? DEFAULT_BOOST : (Float) args[1]; diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/ClusterAllocationExplainRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/ClusterAllocationExplainRequest.java index 0b0bb8c57a9b7..e1c5856e116a5 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/ClusterAllocationExplainRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/ClusterAllocationExplainRequest.java @@ -37,7 +37,7 @@ */ public class ClusterAllocationExplainRequest extends MasterNodeRequest { - private static ObjectParser PARSER = new ObjectParser<>("cluster/allocation/explain"); + private static final ObjectParser PARSER = new ObjectParser<>("cluster/allocation/explain"); static { PARSER.declareString(ClusterAllocationExplainRequest::setIndex, new ParseField("index")); PARSER.declareInt(ClusterAllocationExplainRequest::setShard, new ParseField("shard")); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/RolloverInfo.java b/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/RolloverInfo.java index edb8e3b16e7f4..6dec5ace786f9 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/RolloverInfo.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/rollover/RolloverInfo.java @@ -44,7 +44,7 @@ public class RolloverInfo extends AbstractDiffable implements Writ public static final ParseField TIME_FIELD = new ParseField("time"); @SuppressWarnings("unchecked") - public static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("rollover_info", false, + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("rollover_info", false, (a, alias) -> new RolloverInfo(alias, (List>) a[0], (Long) a[1])); static { PARSER.declareNamedObjects(ConstructingObjectParser.constructorArg(), diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java index 61745d8c10602..d11a88fe50621 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java @@ -44,7 +44,7 @@ public class QueryExplanation implements Writeable, ToXContentFragment { public static final int RANDOM_SHARD = -1; - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "query_explanation", true, a -> { diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponse.java index 2489011fd3d73..9937d98c38ac3 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryResponse.java @@ -48,7 +48,7 @@ public class ValidateQueryResponse extends BroadcastResponse { public static final String EXPLANATIONS_FIELD = "explanations"; @SuppressWarnings("unchecked") - static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "validate_query", true, arg -> { diff --git a/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java b/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java index 616c4e5bd2830..bdf7abaa13d77 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java @@ -176,7 +176,7 @@ public static class Failure implements Writeable, ToXContentFragment { private final long term; private final boolean aborted; - public static ConstructingObjectParser PARSER = + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "bulk_failures", true, diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java index 15598a2a88a5c..9631463c436c4 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java @@ -181,7 +181,7 @@ public static FieldCapabilities fromXContent(String name, XContentParser parser) } @SuppressWarnings("unchecked") - private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "field_capabilities", true, (a, name) -> new FieldCapabilities(name, diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java index 442ed3f68ee9c..905384a76390a 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java @@ -49,7 +49,7 @@ public final class FieldCapabilitiesRequest extends ActionRequest implements Ind // pkg private API mainly for cross cluster search to signal that we do multiple reductions ie. the results should not be merged private boolean mergeResults = true; - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(NAME, FieldCapabilitiesRequest::new); static { diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java b/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java index 5eb327588fb80..3dc8ecbab21fa 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java @@ -206,7 +206,7 @@ private XContentBuilder coordinatesToXContent(Polygon polygon) throws IOExceptio return builder.endObject(); } - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("geojson", true, (a, c) -> { String type = (String) a[0]; CoordinateNode coordinates = (CoordinateNode) a[1]; diff --git a/server/src/main/java/org/elasticsearch/index/query/IdsQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/IdsQueryBuilder.java index def29b91a7f81..03a3fdfd646e6 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IdsQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/IdsQueryBuilder.java @@ -115,8 +115,7 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep builder.endObject(); } - private static ObjectParser PARSER = new ObjectParser<>(NAME, - () -> new IdsQueryBuilder()); + private static final ObjectParser PARSER = new ObjectParser<>(NAME, IdsQueryBuilder::new); static { PARSER.declareStringArray(fromList(String.class, IdsQueryBuilder::addIds), IdsQueryBuilder.VALUES_FIELD); diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreQueryBuilder.java index e55c6d318777a..74e51ff09a683 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreQueryBuilder.java @@ -53,7 +53,7 @@ public class ScriptScoreQueryBuilder extends AbstractQueryBuilder PARSER = new ConstructingObjectParser<>(NAME, false, + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, false, args -> { ScriptScoreQueryBuilder ssQueryBuilder = new ScriptScoreQueryBuilder((QueryBuilder) args[0], (Script) args[1]); if (args[2] != null) ssQueryBuilder.setMinScore((Float) args[2]); diff --git a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java index 8df6620dabc3d..f60b6fc85e137 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java @@ -383,7 +383,7 @@ public static class Status implements Task.Status, SuccessfullyProcessed { } @SuppressWarnings("unchecked") - static ConstructingObjectParser, Void> RETRIES_PARSER = new ConstructingObjectParser<>( + static final ConstructingObjectParser, Void> RETRIES_PARSER = new ConstructingObjectParser<>( "bulk_by_scroll_task_status_retries", true, a -> new Tuple(a[0], a[1]) diff --git a/server/src/main/java/org/elasticsearch/index/seqno/RetentionLease.java b/server/src/main/java/org/elasticsearch/index/seqno/RetentionLease.java index 9cfad7c36ea06..bf667cc29376c 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/RetentionLease.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/RetentionLease.java @@ -145,7 +145,7 @@ public void writeTo(final StreamOutput out) throws IOException { private static final ParseField TIMESTAMP_FIELD = new ParseField("timestamp"); private static final ParseField SOURCE_FIELD = new ParseField("source"); - private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "retention_leases", (a) -> new RetentionLease((String) a[0], (Long) a[1], (Long) a[2], (String) a[3])); diff --git a/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeases.java b/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeases.java index 8c5c282a72d08..c46ba17a946f6 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeases.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/RetentionLeases.java @@ -182,7 +182,7 @@ public void writeTo(final StreamOutput out) throws IOException { private static final ParseField LEASES_FIELD = new ParseField("leases"); @SuppressWarnings("unchecked") - private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "retention_leases", (a) -> new RetentionLeases((Long) a[0], (Long) a[1], (Collection) a[2])); diff --git a/server/src/main/java/org/elasticsearch/script/ScriptContextInfo.java b/server/src/main/java/org/elasticsearch/script/ScriptContextInfo.java index f72b933ae886c..65bbcf2cded82 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptContextInfo.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptContextInfo.java @@ -135,7 +135,7 @@ public List methods() { } @SuppressWarnings("unchecked") - public static ConstructingObjectParser PARSER = + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("script_context_info", true, (m, name) -> new ScriptContextInfo((String) m[0], (List) m[1]) ); @@ -210,7 +210,7 @@ public void writeTo(StreamOutput out) throws IOException { } @SuppressWarnings("unchecked") - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("method", true, (m, name) -> new ScriptMethodInfo((String) m[0], (String) m[1], (List) m[2]) ); @@ -271,7 +271,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(name); } - private static ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("parameters", true, (p) -> new ParameterInfo((String)p[0], (String)p[1]) ); diff --git a/server/src/main/java/org/elasticsearch/script/ScriptLanguagesInfo.java b/server/src/main/java/org/elasticsearch/script/ScriptLanguagesInfo.java index d8bfb4f499fe5..c5225c32caf04 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptLanguagesInfo.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptLanguagesInfo.java @@ -98,7 +98,7 @@ public ScriptLanguagesInfo(StreamInput in) throws IOException { } @SuppressWarnings("unchecked") - public static ConstructingObjectParser PARSER = + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("script_languages_info", true, (a) -> new ScriptLanguagesInfo( new HashSet<>((List)a[0]), @@ -107,7 +107,7 @@ public ScriptLanguagesInfo(StreamInput in) throws IOException { ); @SuppressWarnings("unchecked") - private static ConstructingObjectParser>,Void> LANGUAGE_CONTEXT_PARSER = + private static final ConstructingObjectParser>,Void> LANGUAGE_CONTEXT_PARSER = new ConstructingObjectParser<>("language_contexts", true, (m, name) -> new Tuple<>((String)m[0], Set.copyOf((List)m[1])) ); diff --git a/server/src/main/java/org/elasticsearch/search/SearchHit.java b/server/src/main/java/org/elasticsearch/search/SearchHit.java index 209e1e1b78c74..448b19a34bc83 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchHit.java +++ b/server/src/main/java/org/elasticsearch/search/SearchHit.java @@ -661,7 +661,7 @@ public XContentBuilder toInnerXContent(XContentBuilder builder, Params params) t * of the included search hit. The output of the map is used to create the * actual SearchHit instance via {@link #createFromMap(Map)} */ - private static ObjectParser, Void> MAP_PARSER = new ObjectParser<>("innerHitParser", true, HashMap::new); + private static final ObjectParser, Void> MAP_PARSER = new ObjectParser<>("innerHitParser", true, HashMap::new); static { declareInnerHitsParseFields(MAP_PARSER); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/adjacency/ParsedAdjacencyMatrix.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/adjacency/ParsedAdjacencyMatrix.java index 1fb356d45c28c..b07bbf529d23b 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/adjacency/ParsedAdjacencyMatrix.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/adjacency/ParsedAdjacencyMatrix.java @@ -53,7 +53,7 @@ public ParsedBucket getBucketByKey(String key) { return bucketMap.get(key); } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedAdjacencyMatrix.class.getSimpleName(), true, ParsedAdjacencyMatrix::new); static { declareMultiBucketAggregationFields(PARSER, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/ParsedComposite.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/ParsedComposite.java index e7d6f775f1d87..6dc42c7405d85 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/ParsedComposite.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/ParsedComposite.java @@ -30,7 +30,7 @@ import java.util.Map; public class ParsedComposite extends ParsedMultiBucketAggregation implements CompositeAggregation { - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedComposite.class.getSimpleName(), true, ParsedComposite::new); static { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/ParsedFilters.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/ParsedFilters.java index b2e9c36ccbee8..194c18cc4abb1 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/ParsedFilters.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/ParsedFilters.java @@ -60,7 +60,7 @@ public ParsedBucket getBucketByKey(String key) { return bucketMap.get(key); } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedFilters.class.getSimpleName(), true, ParsedFilters::new); static { declareMultiBucketAggregationFields(PARSER, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/ParsedGeoHashGrid.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/ParsedGeoHashGrid.java index b9af237eb6323..362c220fdff32 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/ParsedGeoHashGrid.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/ParsedGeoHashGrid.java @@ -26,7 +26,7 @@ public class ParsedGeoHashGrid extends ParsedGeoGrid { - private static ObjectParser PARSER = createParser(ParsedGeoHashGrid::new, + private static final ObjectParser PARSER = createParser(ParsedGeoHashGrid::new, ParsedGeoHashGridBucket::fromXContent, ParsedGeoHashGridBucket::fromXContent); public static ParsedGeoGrid fromXContent(XContentParser parser, String name) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/ParsedGeoTileGrid.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/ParsedGeoTileGrid.java index e88c7ad305433..9e07d6af1dc0d 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/ParsedGeoTileGrid.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/ParsedGeoTileGrid.java @@ -26,7 +26,7 @@ public class ParsedGeoTileGrid extends ParsedGeoGrid { - private static ObjectParser PARSER = createParser(ParsedGeoTileGrid::new, + private static final ObjectParser PARSER = createParser(ParsedGeoTileGrid::new, ParsedGeoTileGridBucket::fromXContent, ParsedGeoTileGridBucket::fromXContent); public static ParsedGeoGrid fromXContent(XContentParser parser, String name) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java index 96e5641619caa..6d8afedbb2336 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java @@ -72,7 +72,7 @@ public class DateHistogramAggregationBuilder extends ValuesSourceAggregationBuil implements MultiBucketAggregationBuilder, DateIntervalConsumer { public static final String NAME = "date_histogram"; - private static DateMathParser EPOCH_MILLIS_PARSER = DateFormatter.forPattern("epoch_millis").toDateMathParser(); + private static final DateMathParser EPOCH_MILLIS_PARSER = DateFormatter.forPattern("epoch_millis").toDateMathParser(); public static final Map DATE_FIELD_UNITS = Map.ofEntries( entry("year", Rounding.DateTimeUnit.YEAR_OF_CENTURY), diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedAutoDateHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedAutoDateHistogram.java index 66a29b4e05073..525591f55679b 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedAutoDateHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedAutoDateHistogram.java @@ -52,7 +52,7 @@ public List getBuckets() { return buckets; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedAutoDateHistogram.class.getSimpleName(), true, ParsedAutoDateHistogram::new); static { declareMultiBucketAggregationFields(PARSER, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedDateHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedDateHistogram.java index 1cf43a53ed26c..e212ecc6fb479 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedDateHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedDateHistogram.java @@ -41,7 +41,7 @@ public List getBuckets() { return buckets; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedDateHistogram.class.getSimpleName(), true, ParsedDateHistogram::new); static { declareMultiBucketAggregationFields(PARSER, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedHistogram.java index 6037c1558867a..6e0caac936f37 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ParsedHistogram.java @@ -38,7 +38,7 @@ public List getBuckets() { return buckets; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedHistogram.class.getSimpleName(), true, ParsedHistogram::new); static { declareMultiBucketAggregationFields(PARSER, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedBinaryRange.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedBinaryRange.java index 79b1cd6cc0d09..4aadbb44a4832 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedBinaryRange.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedBinaryRange.java @@ -45,7 +45,7 @@ public List getBuckets() { return buckets; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedBinaryRange.class.getSimpleName(), true, ParsedBinaryRange::new); static { declareMultiBucketAggregationFields(PARSER, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedDateRange.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedDateRange.java index d4504e245541b..3028aec4af24b 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedDateRange.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedDateRange.java @@ -34,7 +34,7 @@ public String getType() { return DateRangeAggregationBuilder.NAME; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedDateRange.class.getSimpleName(), true, ParsedDateRange::new); static { declareParsedRangeFields(PARSER, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedGeoDistance.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedGeoDistance.java index 3a49a8e8de200..5d4afb1e3f093 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedGeoDistance.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedGeoDistance.java @@ -31,7 +31,7 @@ public String getType() { return GeoDistanceAggregationBuilder.NAME; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedGeoDistance.class.getSimpleName(), true, ParsedGeoDistance::new); static { declareParsedRangeFields(PARSER, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedRange.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedRange.java index 6095348e68863..9546ff3784dbc 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedRange.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedRange.java @@ -53,7 +53,7 @@ protected static void declareParsedRangeFields(final ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedRange.class.getSimpleName(), true, ParsedRange::new); static { declareParsedRangeFields(PARSER, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/ParsedSignificantLongTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/ParsedSignificantLongTerms.java index 9592d80c77625..f48d0680698c6 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/ParsedSignificantLongTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/ParsedSignificantLongTerms.java @@ -32,7 +32,7 @@ public String getType() { return SignificantLongTerms.NAME; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedSignificantLongTerms.class.getSimpleName(), true, ParsedSignificantLongTerms::new); static { declareParsedSignificantTermsFields(PARSER, ParsedBucket::fromXContent); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/ParsedSignificantStringTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/ParsedSignificantStringTerms.java index 365cc8b588aec..076aa74457fe3 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/ParsedSignificantStringTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/ParsedSignificantStringTerms.java @@ -34,7 +34,7 @@ public String getType() { return SignificantStringTerms.NAME; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedSignificantStringTerms.class.getSimpleName(), true, ParsedSignificantStringTerms::new); static { declareParsedSignificantTermsFields(PARSER, ParsedBucket::fromXContent); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedDoubleTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedDoubleTerms.java index d3afe5c17603f..02bc3e636bb47 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedDoubleTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedDoubleTerms.java @@ -32,7 +32,7 @@ public String getType() { return DoubleTerms.NAME; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedDoubleTerms.class.getSimpleName(), true, ParsedDoubleTerms::new); static { declareParsedTermsFields(PARSER, ParsedBucket::fromXContent); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedLongTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedLongTerms.java index b5869fc6ee2d8..07185abdcc03c 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedLongTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedLongTerms.java @@ -32,7 +32,7 @@ public String getType() { return LongTerms.NAME; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedLongTerms.class.getSimpleName(), true, ParsedLongTerms::new); static { declareParsedTermsFields(PARSER, ParsedBucket::fromXContent); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedStringTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedStringTerms.java index ecd88c871dfcc..29304ec1871bb 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedStringTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/ParsedStringTerms.java @@ -34,7 +34,7 @@ public String getType() { return StringTerms.NAME; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedStringTerms.class.getSimpleName(), true, ParsedStringTerms::new); static { declareParsedTermsFields(PARSER, ParsedBucket::fromXContent); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedHDRPercentileRanks.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedHDRPercentileRanks.java index eac1f2109056c..278f2423a432f 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedHDRPercentileRanks.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedHDRPercentileRanks.java @@ -49,7 +49,7 @@ public Percentile next() { }; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedHDRPercentileRanks.class.getSimpleName(), true, ParsedHDRPercentileRanks::new); static { ParsedPercentiles.declarePercentilesFields(PARSER); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedHDRPercentiles.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedHDRPercentiles.java index bb34d8550d0ee..60f3a88734388 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedHDRPercentiles.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedHDRPercentiles.java @@ -41,7 +41,7 @@ public String percentileAsString(double percent) { return getPercentileAsString(percent); } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedHDRPercentiles.class.getSimpleName(), true, ParsedHDRPercentiles::new); static { ParsedPercentiles.declarePercentilesFields(PARSER); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTDigestPercentileRanks.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTDigestPercentileRanks.java index f17bc8784aef4..f15dacd74c1a7 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTDigestPercentileRanks.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTDigestPercentileRanks.java @@ -49,7 +49,7 @@ public Percentile next() { }; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedTDigestPercentileRanks.class.getSimpleName(), true, ParsedTDigestPercentileRanks::new); static { ParsedPercentiles.declarePercentilesFields(PARSER); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTDigestPercentiles.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTDigestPercentiles.java index 2453c702b9608..f7a876c16bf12 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTDigestPercentiles.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTDigestPercentiles.java @@ -41,7 +41,7 @@ public String percentileAsString(double percent) { return getPercentileAsString(percent); } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedTDigestPercentiles.class.getSimpleName(), true, ParsedTDigestPercentiles::new); static { ParsedPercentiles.declarePercentilesFields(PARSER); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTopHits.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTopHits.java index 321ed5709e82f..ced2698a52a11 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTopHits.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTopHits.java @@ -47,7 +47,7 @@ protected XContentBuilder doXContentBody(XContentBuilder builder, Params params) return searchHits.toXContent(builder, params); } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedTopHits.class.getSimpleName(), true, ParsedTopHits::new); static { declareAggregationFields(PARSER); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/ParsedPercentilesBucket.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/ParsedPercentilesBucket.java index 360ed9de214b0..93bc53f327947 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/ParsedPercentilesBucket.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/ParsedPercentilesBucket.java @@ -73,7 +73,7 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th return builder; } - private static ObjectParser PARSER = + private static final ObjectParser PARSER = new ObjectParser<>(ParsedPercentilesBucket.class.getSimpleName(), true, ParsedPercentilesBucket::new); static { diff --git a/server/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java b/server/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java index ba03e058a37b9..7a7cf1437888d 100644 --- a/server/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java @@ -563,7 +563,7 @@ public static FieldSortBuilder fromXContent(XContentParser parser, String fieldN return PARSER.parse(parser, new FieldSortBuilder(fieldName), null); } - private static ObjectParser PARSER = new ObjectParser<>(NAME); + private static final ObjectParser PARSER = new ObjectParser<>(NAME); static { PARSER.declareField(FieldSortBuilder::missing, p -> p.objectText(), MISSING, ValueType.VALUE); diff --git a/server/src/main/java/org/elasticsearch/search/sort/ScoreSortBuilder.java b/server/src/main/java/org/elasticsearch/search/sort/ScoreSortBuilder.java index c27979fdc748e..112a5eb2c8c96 100644 --- a/server/src/main/java/org/elasticsearch/search/sort/ScoreSortBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/sort/ScoreSortBuilder.java @@ -86,7 +86,7 @@ public static ScoreSortBuilder fromXContent(XContentParser parser, String fieldN return PARSER.apply(parser, null); } - private static ObjectParser PARSER = new ObjectParser<>(NAME, ScoreSortBuilder::new); + private static final ObjectParser PARSER = new ObjectParser<>(NAME, ScoreSortBuilder::new); static { PARSER.declareString((builder, order) -> builder.order(SortOrder.fromString(order)), ORDER_FIELD); diff --git a/server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java b/server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java index 6cf33830b0453..af766ec463711 100644 --- a/server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java @@ -204,7 +204,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params builderParams) return builder; } - private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, a -> new ScriptSortBuilder((Script) a[0], (ScriptSortType) a[1])); static { diff --git a/server/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestion.java b/server/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestion.java index d9be0c3a4938b..0115ec79b435f 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestion.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestion.java @@ -260,9 +260,8 @@ protected Option newOption(StreamInput in) throws IOException { return new Option(in); } - private static ObjectParser PARSER = new ObjectParser<>("CompletionSuggestionEntryParser", true, + private static final ObjectParser PARSER = new ObjectParser<>("CompletionSuggestionEntryParser", true, Entry::new); - static { declareCommonFields(PARSER); PARSER.declareObjectArray(Entry::addOptions, (p,c) -> Option.fromXContent(p), new ParseField(OPTIONS)); @@ -353,7 +352,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - private static ObjectParser, Void> PARSER = new ObjectParser<>("CompletionOptionParser", + private static final ObjectParser, Void> PARSER = new ObjectParser<>("CompletionOptionParser", true, HashMap::new); static { diff --git a/server/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java b/server/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java index e391f78f27c50..9326e0ff666f7 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java @@ -95,7 +95,7 @@ public int hashCode() { return result; } - private static ObjectParser CATEGORY_PARSER = new ObjectParser<>(NAME, null); + private static final ObjectParser CATEGORY_PARSER = new ObjectParser<>(NAME, null); static { CATEGORY_PARSER.declareField(Builder::setCategory, XContentParser::text, new ParseField(CONTEXT_VALUE), ObjectParser.ValueType.VALUE); diff --git a/server/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoQueryContext.java b/server/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoQueryContext.java index 7885dd1d29674..439fa161d3897 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoQueryContext.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoQueryContext.java @@ -112,7 +112,7 @@ public static Builder builder() { return new Builder(); } - private static ObjectParser GEO_CONTEXT_PARSER = new ObjectParser<>(NAME, null); + private static final ObjectParser GEO_CONTEXT_PARSER = new ObjectParser<>(NAME, null); static { GEO_CONTEXT_PARSER.declareField((parser, geoQueryContext, geoContextMapping) -> geoQueryContext.setGeoPoint(GeoUtils.parseGeoPoint(parser)), diff --git a/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestion.java b/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestion.java index a29c6f08a7d8b..fe7c03b11b1ec 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestion.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestion.java @@ -120,8 +120,7 @@ public void addOption(Option option) { } } - private static ObjectParser PARSER = new ObjectParser<>("PhraseSuggestionEntryParser", true, Entry::new); - + private static final ObjectParser PARSER = new ObjectParser<>("PhraseSuggestionEntryParser", true, Entry::new); static { declareCommonFields(PARSER); PARSER.declareObjectArray(Entry::addOptions, (p, c) -> Option.fromXContent(p), new ParseField(OPTIONS)); diff --git a/server/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestion.java b/server/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestion.java index b6afaf4ba964b..665c95cd47eb3 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestion.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestion.java @@ -179,8 +179,7 @@ protected Option newOption(StreamInput in) throws IOException { return new Option(in); } - private static ObjectParser PARSER = new ObjectParser<>("TermSuggestionEntryParser", true, Entry::new); - + private static final ObjectParser PARSER = new ObjectParser<>("TermSuggestionEntryParser", true, Entry::new); static { declareCommonFields(PARSER); PARSER.declareObjectArray(Entry::addOptions, (p,c) -> Option.fromXContent(p), new ParseField(OPTIONS)); diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeoJsonSerializationTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonSerializationTests.java index 7d1ff1bfc580f..e3974872dee69 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeoJsonSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonSerializationTests.java @@ -47,7 +47,7 @@ public class GeoJsonSerializationTests extends ESTestCase { private static class GeometryWrapper implements ToXContentObject { private Geometry geometry; - private static GeoJson PARSER = new GeoJson(true, false, new GeographyValidator(true)); + private static final GeoJson PARSER = new GeoJson(true, false, new GeographyValidator(true)); GeometryWrapper(Geometry geometry) { this.geometry = geometry; From e8bd6261d1a268a03124a9c743c7916e6360b126 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Fri, 3 Jan 2020 11:55:02 -0500 Subject: [PATCH 376/686] Use Void context on parsers where possible (#50573) *Most* of our parsing can be done without passing any extra context into the parser that isn't already part of the xcontent stream. While I was looking around at the places that *do* need a context I found a few places that were declared to need a context but don't actually need it. --- .../reindex/remote/RemoteResponseParsers.java | 14 +++++++------- .../reindex/remote/RemoteResponseParsersTests.java | 3 +-- .../searchbusinessrules/PinnedQueryBuilder.java | 2 +- .../xpack/sql/action/SqlClearCursorRequest.java | 9 +++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/remote/RemoteResponseParsers.java b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/remote/RemoteResponseParsers.java index 1029b76d0f94b..efa81aab6dc82 100644 --- a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/remote/RemoteResponseParsers.java +++ b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/remote/RemoteResponseParsers.java @@ -124,7 +124,7 @@ class Fields { /** * Parser for {@code failed} shards in the {@code _shards} elements. */ - public static final ConstructingObjectParser SEARCH_FAILURE_PARSER = + public static final ConstructingObjectParser SEARCH_FAILURE_PARSER = new ConstructingObjectParser<>("failure", true, a -> { int i = 0; String index = (String) a[i++]; @@ -146,7 +146,7 @@ class Fields { SEARCH_FAILURE_PARSER.declareString(optionalConstructorArg(), new ParseField("node")); SEARCH_FAILURE_PARSER.declareField(constructorArg(), (p, c) -> { if (p.currentToken() == XContentParser.Token.START_OBJECT) { - return ThrowableBuilder.PARSER.apply(p, c); + return ThrowableBuilder.PARSER.apply(p, null); } else { return p.text(); } @@ -157,7 +157,7 @@ class Fields { * Parser for the {@code _shards} element. Throws everything out except the errors array if there is one. If there isn't one then it * parses to an empty list. */ - public static final ConstructingObjectParser, XContentType> SHARDS_PARSER = + public static final ConstructingObjectParser, Void> SHARDS_PARSER = new ConstructingObjectParser<>("_shards", true, a -> { @SuppressWarnings("unchecked") List failures = (List) a[0]; @@ -196,20 +196,20 @@ class Fields { return new Response(timedOut, failures, totalHits, hits, scroll); }); static { - RESPONSE_PARSER.declareObject(optionalConstructorArg(), ThrowableBuilder.PARSER::apply, new ParseField("error")); + RESPONSE_PARSER.declareObject(optionalConstructorArg(), (p, c) -> ThrowableBuilder.PARSER.apply(p, null), new ParseField("error")); RESPONSE_PARSER.declareBoolean(optionalConstructorArg(), new ParseField("timed_out")); RESPONSE_PARSER.declareString(optionalConstructorArg(), new ParseField("_scroll_id")); RESPONSE_PARSER.declareObject(optionalConstructorArg(), HITS_PARSER, new ParseField("hits")); - RESPONSE_PARSER.declareObject(optionalConstructorArg(), SHARDS_PARSER, new ParseField("_shards")); + RESPONSE_PARSER.declareObject(optionalConstructorArg(), (p, c) -> SHARDS_PARSER.apply(p, null), new ParseField("_shards")); } /** * Collects stuff about Throwables and attempts to rebuild them. */ public static class ThrowableBuilder { - public static final BiFunction PARSER; + public static final BiFunction PARSER; static { - ObjectParser parser = new ObjectParser<>("reason", true, ThrowableBuilder::new); + ObjectParser parser = new ObjectParser<>("reason", true, ThrowableBuilder::new); PARSER = parser.andThen(ThrowableBuilder::build); parser.declareString(ThrowableBuilder::setType, new ParseField("type")); parser.declareString(ThrowableBuilder::setReason, new ParseField("reason")); diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteResponseParsersTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteResponseParsersTests.java index f5b406f779a9e..61a8320696439 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteResponseParsersTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteResponseParsersTests.java @@ -24,7 +24,6 @@ import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.reindex.ScrollableHitSource; import org.elasticsearch.test.ESTestCase; import org.hamcrest.Matchers; @@ -43,7 +42,7 @@ public void testFailureWithoutIndex() throws IOException { XContentBuilder builder = jsonBuilder(); failure.toXContent(builder, ToXContent.EMPTY_PARAMS); try (XContentParser parser = createParser(builder)) { - ScrollableHitSource.SearchFailure parsed = RemoteResponseParsers.SEARCH_FAILURE_PARSER.parse(parser, XContentType.JSON); + ScrollableHitSource.SearchFailure parsed = RemoteResponseParsers.SEARCH_FAILURE_PARSER.parse(parser, null); assertNotNull(parsed.getReason()); assertThat(parsed.getReason().getMessage(), Matchers.containsString("exhausted")); assertThat(parsed.getReason(), Matchers.instanceOf(EsRejectedExecutionException.class)); diff --git a/x-pack/plugin/search-business-rules/src/main/java/org/elasticsearch/xpack/searchbusinessrules/PinnedQueryBuilder.java b/x-pack/plugin/search-business-rules/src/main/java/org/elasticsearch/xpack/searchbusinessrules/PinnedQueryBuilder.java index 5e2adc5c67440..5cca38245ce71 100644 --- a/x-pack/plugin/search-business-rules/src/main/java/org/elasticsearch/xpack/searchbusinessrules/PinnedQueryBuilder.java +++ b/x-pack/plugin/search-business-rules/src/main/java/org/elasticsearch/xpack/searchbusinessrules/PinnedQueryBuilder.java @@ -133,7 +133,7 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, a -> { QueryBuilder organicQuery = (QueryBuilder) a[0]; diff --git a/x-pack/plugin/sql/sql-action/src/main/java/org/elasticsearch/xpack/sql/action/SqlClearCursorRequest.java b/x-pack/plugin/sql/sql-action/src/main/java/org/elasticsearch/xpack/sql/action/SqlClearCursorRequest.java index 4d5cc1734333f..563b1fe7cf886 100644 --- a/x-pack/plugin/sql/sql-action/src/main/java/org/elasticsearch/xpack/sql/action/SqlClearCursorRequest.java +++ b/x-pack/plugin/sql/sql-action/src/main/java/org/elasticsearch/xpack/sql/action/SqlClearCursorRequest.java @@ -19,16 +19,17 @@ import static org.elasticsearch.action.ValidateActions.addValidationError; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; +import static org.elasticsearch.xpack.sql.action.AbstractSqlQueryRequest.CLIENT_ID; import static org.elasticsearch.xpack.sql.action.AbstractSqlQueryRequest.CURSOR; import static org.elasticsearch.xpack.sql.action.AbstractSqlQueryRequest.MODE; -import static org.elasticsearch.xpack.sql.action.AbstractSqlQueryRequest.CLIENT_ID; /** * Request to clean all SQL resources associated with the cursor */ public class SqlClearCursorRequest extends AbstractSqlRequest { - private static final ConstructingObjectParser PARSER = + private static final ConstructingObjectParser PARSER = // here the position in "objects" is the same as the fields parser declarations below new ConstructingObjectParser<>(SqlClearCursorAction.NAME, objects -> { RequestInfo requestInfo = new RequestInfo(Mode.fromString((String) objects[1]), @@ -39,8 +40,8 @@ public class SqlClearCursorRequest extends AbstractSqlRequest { static { // "cursor" is required constructor parameter PARSER.declareString(constructorArg(), CURSOR); - PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), MODE); - PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), CLIENT_ID); + PARSER.declareString(optionalConstructorArg(), MODE); + PARSER.declareString(optionalConstructorArg(), CLIENT_ID); } private String cursor; From bafbc235d59c6304a4bb06d4f7f63c664b2a116b Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Fri, 3 Jan 2020 11:58:29 -0500 Subject: [PATCH 377/686] x-content: Support collapsed named objects (#50564) This adds support for "collapsed" named object to `ObjectParser`. In particular, this supports the sort of xcontent that we use to specify significance heuristics. See #25519 and this example: ``` GET /_search { "query" : { "terms" : {"force" : [ "British Transport Police" ]} }, "aggregations" : { "significant_crime_types" : { "significant_terms" : { "field" : "crime_type", "mutual_information" : { <<------- This is the name "include_negatives": true } } } } } ``` I believe there are a couple of things that work this way. I've held off on moving the actual parsing of the significant heuristics to this code to keep the review more compact. The moving is pretty mechanical stuff in the aggs framework. --- .../common/xcontent/ObjectParser.java | 42 +++++++++++++-- .../xcontent/XContentParseException.java | 8 ++- .../common/xcontent/ObjectParserTests.java | 52 +++++++++++++++++++ 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java index c80c5bdb0d09a..09a997e7a1516 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java @@ -130,21 +130,36 @@ private static UnknownFieldParser consumeUnknow }; } + private static UnknownFieldParser unknownIsNamedXContent( + Class categoryClass, + BiConsumer consumer + ) { + return (parserName, field, location, parser, value, context) -> { + Category o; + try { + o = parser.namedObject(categoryClass, field, context); + } catch (NamedObjectNotFoundException e) { + throw new XContentParseException(location, "[" + parserName + "] " + e.getBareMessage(), e); + } + consumer.accept(value, o); + }; + } + private final Map fieldParserMap = new HashMap<>(); private final String name; private final Supplier valueSupplier; - private final UnknownFieldParser unknownFieldParser; /** - * Creates a new ObjectParser instance with a name. This name is used to reference the parser in exceptions and messages. + * Creates a new ObjectParser. + * @param name the parsers name, used to reference the parser in exceptions and messages. */ public ObjectParser(String name) { this(name, null); } /** - * Creates a new ObjectParser instance with a name. + * Creates a new ObjectParser. * @param name the parsers name, used to reference the parser in exceptions and messages. * @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser. */ @@ -153,7 +168,7 @@ public ObjectParser(String name, @Nullable Supplier valueSupplier) { } /** - * Creates a new ObjectParser instance with a name. + * Creates a new ObjectParser. * @param name the parsers name, used to reference the parser in exceptions and messages. * @param ignoreUnknownFields Should this parser ignore unknown fields? This should generally be set to true only when parsing * responses from external systems, never when parsing requests from users. @@ -164,7 +179,7 @@ public ObjectParser(String name, boolean ignoreUnknownFields, @Nullable Supplier } /** - * Creates a new ObjectParser instance with a name. + * Creates a new ObjectParser that consumes unknown fields as generic Objects. * @param name the parsers name, used to reference the parser in exceptions and messages. * @param unknownFieldConsumer how to consume parsed unknown fields * @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser. @@ -173,6 +188,23 @@ public ObjectParser(String name, UnknownFieldConsumer unknownFieldConsume this(name, consumeUnknownField(unknownFieldConsumer), valueSupplier); } + /** + * Creates a new ObjectParser that attempts to resolve unknown fields as {@link XContentParser#namedObject namedObjects}. + * @param the type of named object that unknown fields are expected to be + * @param name the parsers name, used to reference the parser in exceptions and messages. + * @param categoryClass the type of named object that unknown fields are expected to be + * @param unknownFieldConsumer how to consume parsed unknown fields + * @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser. + */ + public ObjectParser( + String name, + Class categoryClass, + BiConsumer unknownFieldConsumer, + @Nullable Supplier valueSupplier + ) { + this(name, unknownIsNamedXContent(categoryClass, unknownFieldConsumer), valueSupplier); + } + /** * Creates a new ObjectParser instance with a name. * @param name the parsers name, used to reference the parser in exceptions and messages. diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentParseException.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentParseException.java index 69c345d20c2a6..cf9d4ed6713d9 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentParseException.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentParseException.java @@ -59,7 +59,13 @@ public XContentLocation getLocation() { @Override public String getMessage() { - return location.map(l -> "[" + l.toString() + "] ").orElse("") + super.getMessage(); + return location.map(l -> "[" + l.toString() + "] ").orElse("") + getBareMessage(); } + /** + * Get the exception message without location information. + */ + public String getBareMessage() { + return super.getMessage(); + } } diff --git a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java index a2c45eceb8d58..fb5db61935a27 100644 --- a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java +++ b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java @@ -753,4 +753,56 @@ public void testConsumeUnknownFields() throws IOException { assertEquals(List.of(1, 2, 3, 4), o.fields.get("test_array")); assertEquals(Map.of("field", "value", "field2", List.of("list1", "list2")), o.fields.get("test_nested")); } + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(Arrays.asList( + new NamedXContentRegistry.Entry(Object.class, new ParseField("str"), p -> p.text()), + new NamedXContentRegistry.Entry(Object.class, new ParseField("int"), p -> p.intValue()), + new NamedXContentRegistry.Entry(Object.class, new ParseField("float"), p -> p.floatValue()), + new NamedXContentRegistry.Entry(Object.class, new ParseField("bool"), p -> p.booleanValue()) + )); + } + + private static class TopLevelNamedXConent { + public static final ObjectParser PARSER = new ObjectParser<>( + "test", Object.class, TopLevelNamedXConent::setNamed, TopLevelNamedXConent::new + ); + + Object named; + void setNamed(Object named) { + if (this.named != null) { + throw new IllegalArgumentException("Only one [named] allowed!"); + } + this.named = named; + } + } + + public void testTopLevelNamedXContent() throws IOException { + { + XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"str\": \"foo\"}"); + TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null); + assertEquals("foo", o.named); + } + { + XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"int\": 1}"); + TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null); + assertEquals(1, o.named); + } + { + XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"float\": 4.0}"); + TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null); + assertEquals(4.0F, o.named); + } + { + XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"bool\": false}"); + TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null); + assertEquals(false, o.named); + } + { + XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}"); + XContentParseException ex = expectThrows(XContentParseException.class, () -> TopLevelNamedXConent.PARSER.parse(parser, null)); + assertEquals("[1:2] [test] unable to parse Object with name [not_supported_field]: parser not found", ex.getMessage()); + } + } } From 16535e84c35cf1342829fdcdb3d739c95438c6f3 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 3 Jan 2020 09:07:08 -0800 Subject: [PATCH 378/686] [DOCS] Adds missing timing_stats descriptions (#50574) --- .../anomaly-detection/apis/get-datafeed-stats.asciidoc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc b/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc index 695c16761a623..21a907a703c9a 100644 --- a/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/get-datafeed-stats.asciidoc @@ -96,9 +96,12 @@ re-started. `timing_stats`:: (object) An object that provides statistical information about timing aspect of this {dfeed}. -//average_search_time_per_bucket_ms -//bucket_count -//exponential_average_search_time_per_hour_ms +`timing_stats`.`average_search_time_per_bucket_ms`::: +(double) Average of the {dfeed} search times in milliseconds. +`timing_stats`.`bucket_count`::: +(long) The number of buckets processed. +`timing_stats`.`exponential_average_search_time_per_hour_ms`::: +(double) Exponential moving average of the {dfeed} search times in milliseconds. `timing_stats`.`job_id`::: include::{docdir}/ml/ml-shared.asciidoc[tag=job-id-anomaly-detection] `timing_stats`.`search_count`::: Number of searches performed by this {dfeed}. From 4a6f4269b8433bdd526b6fa6f69537081c1ff8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Fri, 3 Jan 2020 18:25:02 +0100 Subject: [PATCH 379/686] Mute TimeSeriesLifecycleActionsIT.testHistoryIsWrittenWithSuccess Also muting TimeSeriesLifecycleActionsIT.testHistoryIsWrittenWithFailure. Tracked in #50353 --- .../xpack/ilm/TimeSeriesLifecycleActionsIT.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 7cb8d92789169..16ef7a9639542 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -1085,6 +1085,7 @@ public void testRolloverStepRetriesUntilRolledOverIndexIsDeleted() throws Except assertBusy(() -> assertThat(getStepKeyForIndex(index), equalTo(TerminalPolicyStep.KEY))); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50353") public void testHistoryIsWrittenWithSuccess() throws Exception { String index = "index"; @@ -1127,6 +1128,8 @@ public void testHistoryIsWrittenWithSuccess() throws Exception { assertBusy(() -> assertHistoryIsPresent(policy, index + "-000002", true, "check-rollover-ready"), 30, TimeUnit.SECONDS); } + + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50353") public void testHistoryIsWrittenWithFailure() throws Exception { String index = "index"; @@ -1361,7 +1364,9 @@ private static StepKey getStepKey(Map explainIndexResponse) { private String getFailedStepForIndex(String indexName) throws IOException { Map indexResponse = explainIndex(indexName); - if (indexResponse == null) return null; + if (indexResponse == null) { + return null; + } return (String) indexResponse.get("failed_step"); } From 6ce4ed9655bd32bcfcf9f7528ef9471c039f97ee Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 3 Jan 2020 20:46:00 +0100 Subject: [PATCH 380/686] Make EC2 Discovery Cache Empty Seed Hosts List (#50607) Follow up to #50550. Cache empty nodes lists (`fetchDynamicNodes` will return an empty list in case of failure) now that the plugin properly retries requests to AWS EC2 APIs. --- .../discovery/ec2/AwsEc2SeedHostsProvider.java | 13 ++----------- .../discovery/ec2/Ec2DiscoveryTests.java | 4 ++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2SeedHostsProvider.java b/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2SeedHostsProvider.java index 515aef8408b01..dd4ef02c0b318 100644 --- a/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2SeedHostsProvider.java +++ b/plugins/discovery-ec2/src/main/java/org/elasticsearch/discovery/ec2/AwsEc2SeedHostsProvider.java @@ -53,7 +53,7 @@ import static org.elasticsearch.discovery.ec2.AwsEc2Service.HostType.TAG_PREFIX; class AwsEc2SeedHostsProvider implements SeedHostsProvider { - + private static final Logger logger = LogManager.getLogger(AwsEc2SeedHostsProvider.class); private final TransportService transportService; @@ -221,22 +221,13 @@ private DescribeInstancesRequest buildDescribeInstancesRequest() { private final class TransportAddressesCache extends SingleObjectCache> { - private boolean empty = true; - protected TransportAddressesCache(TimeValue refreshInterval) { super(refreshInterval, new ArrayList<>()); } - @Override - protected boolean needsRefresh() { - return (empty || super.needsRefresh()); - } - @Override protected List refresh() { - final List nodes = fetchDynamicNodes(); - empty = nodes.isEmpty(); - return nodes; + return fetchDynamicNodes(); } } } diff --git a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryTests.java b/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryTests.java index 6703812a4ec0c..ba31848464310 100644 --- a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryTests.java +++ b/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryTests.java @@ -306,10 +306,10 @@ protected List fetchDynamicNodes() { return new ArrayList<>(); } }; - for (int i=0; i<3; i++) { + for (int i = 0; i < 3; i++) { provider.getSeedAddresses(null); } - assertThat(provider.fetchCount, is(3)); + assertThat(provider.fetchCount, is(1)); } public void testGetNodeListCached() throws Exception { From 9f47b06deedfe7d36443d4b96db338a7e7c55c6e Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Sun, 5 Jan 2020 14:42:34 -0500 Subject: [PATCH 381/686] Clean up wire test case a bit (#50627) * Adds JavaDoc to `AbstractWireTestCase` and `AbstractWireSerializingTestCase` so it is more obvious you should prefer the latter if you have a choice * Moves the `instanceReader` method out of `AbstractWireTestCase` becaue it is no longer used. * Marks a bunch of methods final so it is more obvious which classes are for what. * Cleans up the side effects of the above. --- .../geometry/BaseGeometryTestCase.java | 7 ---- .../rollover/RolloverResponseTests.java | 11 ++----- .../test/AbstractWireSerializingTestCase.java | 14 +++++++- .../test/AbstractWireTestCase.java | 32 +++++++++++++------ .../AbstractSqlWireSerializingTestCase.java | 5 +++ .../xpack/sql/session/ListCursorTests.java | 12 ++----- 6 files changed, 45 insertions(+), 36 deletions(-) diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/BaseGeometryTestCase.java b/libs/geo/src/test/java/org/elasticsearch/geometry/BaseGeometryTestCase.java index ae2dd17e4285b..9cf05ca1a005f 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/BaseGeometryTestCase.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/BaseGeometryTestCase.java @@ -21,7 +21,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; -import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.geometry.utils.GeographyValidator; import org.elasticsearch.geometry.utils.WellKnownText; import org.elasticsearch.test.AbstractWireTestCase; @@ -42,12 +41,6 @@ protected final T createTestInstance() { protected abstract T createTestInstance(boolean hasAlt); - @Override - protected Writeable.Reader instanceReader() { - throw new IllegalStateException("shouldn't be called in this test"); - } - - @SuppressWarnings("unchecked") @Override protected T copyInstance(T instance, Version version) throws IOException { diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/rollover/RolloverResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/rollover/RolloverResponseTests.java index e268ed481e2cd..7264b00f83ff3 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/rollover/RolloverResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/rollover/RolloverResponseTests.java @@ -19,20 +19,18 @@ package org.elasticsearch.action.admin.indices.rollover; -import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.test.AbstractWireTestCase; +import org.elasticsearch.test.AbstractWireSerializingTestCase; -import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Supplier; -public class RolloverResponseTests extends AbstractWireTestCase { +public class RolloverResponseTests extends AbstractWireSerializingTestCase { @Override protected RolloverResponse createTestInstance() { @@ -65,11 +63,6 @@ protected Writeable.Reader instanceReader() { return RolloverResponse::new; } - @Override - protected RolloverResponse copyInstance(RolloverResponse instance, Version version) throws IOException { - return copyWriteable(instance, writableRegistry(), RolloverResponse::new); - } - @Override protected RolloverResponse mutateInstance(RolloverResponse response) { int i = randomIntBetween(0, 6); diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractWireSerializingTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractWireSerializingTestCase.java index cb7f5ff4a229e..ce1e5f7ce9713 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractWireSerializingTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractWireSerializingTestCase.java @@ -19,14 +19,26 @@ package org.elasticsearch.test; import org.elasticsearch.Version; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import java.io.IOException; +/** + * Standard test case for testing the wire serialization of subclasses of {@linkplain Writeable}. + */ public abstract class AbstractWireSerializingTestCase extends AbstractWireTestCase { + /** + * Returns a {@link Writeable.Reader} that can be used to de-serialize the instance + */ + protected abstract Writeable.Reader instanceReader(); + /** + * Copy the {@link Writeable} by round tripping it through {@linkplain StreamInput} and {@linkplain StreamOutput}. + */ @Override - protected T copyInstance(T instance, Version version) throws IOException { + protected final T copyInstance(T instance, Version version) throws IOException { return copyWriteable(instance, getNamedWriteableRegistry(), instanceReader(), version); } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractWireTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractWireTestCase.java index 3e58dc8809d5d..3997db194e9b5 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractWireTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractWireTestCase.java @@ -27,6 +27,10 @@ import java.io.IOException; import java.util.Collections; +/** + * Standard test case for testing wire serialization. If the class being tested + * extends {@link Writeable} then prefer extending {@link AbstractWireSerializingTestCase}. + */ public abstract class AbstractWireTestCase extends ESTestCase { protected static final int NUMBER_OF_TEST_RUNS = 20; @@ -38,11 +42,6 @@ public abstract class AbstractWireTestCase extends ESTestCase { */ protected abstract T createTestInstance(); - /** - * Returns a {@link Writeable.Reader} that can be used to de-serialize the instance - */ - protected abstract Writeable.Reader instanceReader(); - /** * Returns an instance which is mutated slightly so it should not be equal * to the given instance. @@ -73,18 +72,26 @@ public final void testSerialization() throws IOException { } /** - * Serialize the given instance and asserts that both are equal + * Serialize the given instance and asserts that both are equal. */ - protected final T assertSerialization(T testInstance) throws IOException { - return assertSerialization(testInstance, Version.CURRENT); + protected final void assertSerialization(T testInstance) throws IOException { + assertSerialization(testInstance, Version.CURRENT); } - protected final T assertSerialization(T testInstance, Version version) throws IOException { + /** + * Assert that instances copied at a particular version are equal. The version is useful + * for sanity checking the backwards compatibility of the wire. It isn't a substitute for + * real backwards compatibility tests but it is *so* much faster. + */ + protected final void assertSerialization(T testInstance, Version version) throws IOException { T deserializedInstance = copyInstance(testInstance, version); assertEqualInstances(testInstance, deserializedInstance); - return deserializedInstance; } + /** + * Assert that two instances are equal. This is intentionally not final so we can override + * how equality is checked. + */ protected void assertEqualInstances(T expectedInstance, T newInstance) { assertNotSame(newInstance, expectedInstance); assertEquals(expectedInstance, newInstance); @@ -95,6 +102,11 @@ protected final T copyInstance(T instance) throws IOException { return copyInstance(instance, Version.CURRENT); } + /** + * Copy the instance as by reading and writing using the code specific to the provided version. + * The version is useful for sanity checking the backwards compatibility of the wire. It isn't + * a substitute for real backwards compatibility tests but it is *so* much faster. + */ protected abstract T copyInstance(T instance, Version version) throws IOException; /** diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/AbstractSqlWireSerializingTestCase.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/AbstractSqlWireSerializingTestCase.java index 057cc76ee0226..6f8dae854f48d 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/AbstractSqlWireSerializingTestCase.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/AbstractSqlWireSerializingTestCase.java @@ -32,6 +32,11 @@ protected T copyInstance(T instance, Version version) throws IOException { } } } + + /** + * Returns a {@link Writeable.Reader} that can be used to de-serialize the instance + */ + protected abstract Writeable.Reader instanceReader(); protected ZoneId instanceZoneId(T instance) { return randomSafeZone(); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/session/ListCursorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/session/ListCursorTests.java index ac14465451eb4..2fd88e3c0bc8c 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/session/ListCursorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/session/ListCursorTests.java @@ -7,15 +7,14 @@ import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.io.stream.Writeable.Reader; -import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.test.AbstractWireTestCase; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class ListCursorTests extends AbstractWireSerializingTestCase { +public class ListCursorTests extends AbstractWireTestCase { public static ListCursor randomPagingListCursor() { int size = between(1, 20); int depth = between(1, 20); @@ -45,17 +44,12 @@ protected ListCursor createTestInstance() { return randomPagingListCursor(); } - @Override - protected Reader instanceReader() { - return ListCursor::new; - } - @Override protected ListCursor copyInstance(ListCursor instance, Version version) throws IOException { /* Randomly choose between internal protocol round trip and String based * round trips used to toXContent. */ if (randomBoolean()) { - return super.copyInstance(instance, version); + return copyWriteable(instance, getNamedWriteableRegistry(), ListCursor::new, version); } return (ListCursor) Cursors.decodeFromString(Cursors.encodeToString(instance, randomZone())); } From 8906189e952f51a2cfe2c81b523b9f6b33c8fcda Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Sun, 5 Jan 2020 18:30:17 -0500 Subject: [PATCH 382/686] Deprecate indices without soft-deletes (#50502) Soft-deletes will be enabled for all indices in 8.0. Hence, we should deprecate new indices without soft-deletes in 7.x. --- .../index-modules/history-retention.asciidoc | 3 ++ .../elasticsearch/upgrades/RecoveryIT.java | 29 ++++++++++++-- .../test/indices.create/10_basic.yml | 17 +++++++++ .../test/indices.stats/20_translog.yml | 25 +++++++++--- .../metadata/MetaDataCreateIndexService.java | 8 ++++ .../MetaDataCreateIndexServiceTests.java | 15 ++++++++ .../test/rest/ESRestTestCase.java | 38 ++++++++++++++----- 7 files changed, 115 insertions(+), 20 deletions(-) diff --git a/docs/reference/index-modules/history-retention.asciidoc b/docs/reference/index-modules/history-retention.asciidoc index 94e17e49251e3..8df79625c88d0 100644 --- a/docs/reference/index-modules/history-retention.asciidoc +++ b/docs/reference/index-modules/history-retention.asciidoc @@ -65,6 +65,9 @@ there>>. {ccr-cap} will not function if soft deletes are disabled. configured at index creation and only on indices created on or after 6.5.0. The default value is `true`. + deprecated::[7.6, Creating indices with soft-deletes disabled is + deprecated and will be removed in future Elasticsearch versions.] + `index.soft_deletes.retention_lease.period`:: The maximum length of time to retain a shard history retention lease before diff --git a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java index 080b15268db1d..88dabadbd09fd 100644 --- a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java +++ b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java @@ -28,6 +28,7 @@ import org.elasticsearch.cluster.metadata.MetaDataIndexStateService; import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; import org.elasticsearch.common.Booleans; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.xcontent.support.XContentMapValues; @@ -281,7 +282,7 @@ public void testRelocationWithConcurrentIndexing() throws Exception { } } - public void testRecoveryWithSoftDeletes() throws Exception { + public void testRecovery() throws Exception { final String index = "recover_with_soft_deletes"; if (CLUSTER_TYPE == ClusterType.OLD) { Settings.Builder settings = Settings.builder() @@ -294,7 +295,7 @@ public void testRecoveryWithSoftDeletes() throws Exception { .put(INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), "100ms") .put(SETTING_ALLOCATION_MAX_RETRY.getKey(), "0"); // fail faster if (randomBoolean()) { - settings.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true); + settings.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()); } createIndex(index, settings.build()); int numDocs = randomInt(10); @@ -325,7 +326,7 @@ public void testRetentionLeasesEstablishedWhenPromotingPrimary() throws Exceptio .put(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), between(1, 2)) // triggers nontrivial promotion .put(INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), "100ms") .put(SETTING_ALLOCATION_MAX_RETRY.getKey(), "0") // fail faster - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true); + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()); createIndex(index, settings.build()); int numDocs = randomInt(10); indexDocs(index, 0, numDocs); @@ -348,7 +349,7 @@ public void testRetentionLeasesEstablishedWhenRelocatingPrimary() throws Excepti .put(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), between(0, 1)) .put(INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), "100ms") .put(SETTING_ALLOCATION_MAX_RETRY.getKey(), "0") // fail faster - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true); + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()); createIndex(index, settings.build()); int numDocs = randomInt(10); indexDocs(index, 0, numDocs); @@ -740,6 +741,26 @@ public void testAutoExpandIndicesDuringRollingUpgrade() throws Exception { } } + public void testSoftDeletesDisabledWarning() throws Exception { + final String indexName = "test_soft_deletes_disabled_warning"; + if (CLUSTER_TYPE == ClusterType.OLD) { + boolean softDeletesEnabled = true; + Settings.Builder settings = Settings.builder(); + if (randomBoolean()) { + softDeletesEnabled = randomBoolean(); + settings.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), softDeletesEnabled); + } + Request request = new Request("PUT", "/" + indexName); + request.setJsonEntity("{\"settings\": " + Strings.toString(settings.build()) + "}"); + if (softDeletesEnabled == false) { + expectSoftDeletesWarning(request, indexName); + } + client().performRequest(request); + } + ensureGreen(indexName); + indexDocs(indexName, randomInt(100), randomInt(100)); + } + @SuppressWarnings("unchecked") private Map getIndexSettingsAsMap(String index) throws IOException { Map indexSettings = getIndexSettings(index); diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml index 7e15ba70c16ba..1dacfd6ad61c3 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml @@ -119,3 +119,20 @@ properties: "": type: keyword + +--- +"Create index without soft deletes": + - skip: + version: " - 7.9.99" + reason: "indices without soft deletes are deprecated in 8.0" + features: "warnings" + + - do: + warnings: + - Creating indices with soft-deletes disabled is deprecated and will be removed in future Elasticsearch versions. + Please do not specify value for setting [index.soft_deletes.enabled] of index [test_index]. + indices.create: + index: test_index + body: + settings: + soft_deletes.enabled: false diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.stats/20_translog.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.stats/20_translog.yml index c522b27a0be97..8ba11bb280ce7 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.stats/20_translog.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.stats/20_translog.yml @@ -1,11 +1,19 @@ --- "Translog retention without soft_deletes": + - skip: + version: " - 7.9.99" + reason: "indices without soft deletes are deprecated in 8.0" + features: "warnings" + - do: indices.create: index: test body: settings: soft_deletes.enabled: false + warnings: + - Creating indices with soft-deletes disabled is deprecated and will be removed in future Elasticsearch versions. + Please do not specify value for setting [index.soft_deletes.enabled] of index [test]. - do: cluster.health: wait_for_no_initializing_shards: true @@ -68,8 +76,8 @@ --- "Translog retention with soft_deletes": - skip: - version: " - 7.9.99" - reason: "start ignoring translog retention policy with soft-deletes enabled in 8.0" + version: " - 7.3.99" + reason: "start ignoring translog retention policy with soft-deletes enabled in 7.4" - do: indices.create: index: test @@ -130,8 +138,9 @@ --- "Translog stats on closed indices without soft-deletes": - skip: - version: " - 7.2.99" - reason: "closed indices have translog stats starting version 7.3.0" + version: " - 7.9.99" + reason: "indices without soft deletes are deprecated in 8.0" + features: "warnings" - do: indices.create: @@ -139,6 +148,10 @@ body: settings: soft_deletes.enabled: false + warnings: + - Creating indices with soft-deletes disabled is deprecated and will be removed in future Elasticsearch versions. + Please do not specify value for setting [index.soft_deletes.enabled] of index [test]. + - do: cluster.health: wait_for_no_initializing_shards: true @@ -184,8 +197,8 @@ --- "Translog stats on closed indices with soft-deletes": - skip: - version: " - 7.9.99" - reason: "start ignoring translog retention policy with soft-deletes enabled in 8.0" + version: " - 7.3.99" + reason: "start ignoring translog retention policy with soft-deletes enabled in 7.4" - do: indices.create: index: test diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java index 9e310280636a0..1ed7fb04407f1 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java @@ -53,6 +53,7 @@ import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.io.PathUtils; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; @@ -62,6 +63,7 @@ import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.MapperService.MergeReason; @@ -102,6 +104,7 @@ */ public class MetaDataCreateIndexService { private static final Logger logger = LogManager.getLogger(MetaDataCreateIndexService.class); + private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(logger); public static final int MAX_INDEX_NAME_BYTES = 255; @@ -434,6 +437,11 @@ static Settings aggregateIndexSettings(ClusterState currentState, CreateIndexClu * that will be used to create this index. */ MetaDataCreateIndexService.checkShardLimit(indexSettings, currentState); + if (indexSettings.getAsBoolean(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) == false) { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("soft_deletes_disabled", + "Creating indices with soft-deletes disabled is deprecated and will be removed in future Elasticsearch versions. " + + "Please do not specify value for setting [index.soft_deletes.enabled] of index [" + request.index() + "]."); + } return indexSettings; } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexServiceTests.java index e7d35eaeb18ab..e90f710af0f45 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexServiceTests.java @@ -828,6 +828,21 @@ public void testGetIndexNumberOfRoutingShardsYieldsSourceNumberOfShards() { assertThat(targetRoutingNumberOfShards, is(6)); } + public void testSoftDeletesDisabledDeprecation() { + request = new CreateIndexClusterStateUpdateRequest("create index", "test", "test"); + request.settings(Settings.builder().put(INDEX_SOFT_DELETES_SETTING.getKey(), false).build()); + aggregateIndexSettings(ClusterState.EMPTY_STATE, request, List.of(), Map.of(), + null, Settings.EMPTY, IndexScopedSettings.DEFAULT_SCOPED_SETTINGS); + assertWarnings("Creating indices with soft-deletes disabled is deprecated and will be removed in future Elasticsearch versions. " + + "Please do not specify value for setting [index.soft_deletes.enabled] of index [test]."); + request = new CreateIndexClusterStateUpdateRequest("create index", "test", "test"); + if (randomBoolean()) { + request.settings(Settings.builder().put(INDEX_SOFT_DELETES_SETTING.getKey(), true).build()); + } + aggregateIndexSettings(ClusterState.EMPTY_STATE, request, List.of(), Map.of(), + null, Settings.EMPTY, IndexScopedSettings.DEFAULT_SCOPED_SETTINGS); + } + private IndexTemplateMetaData addMatchingTemplate(Consumer configurator) { IndexTemplateMetaData.Builder builder = templateMetaDataBuilder("template1", "te*"); configurator.accept(builder); diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index d5da4320ab3ee..4344b54c38eda 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -52,6 +52,7 @@ import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.snapshots.SnapshotState; @@ -964,23 +965,27 @@ protected static void ensureNoInitializingShards() throws IOException { } protected static void createIndex(String name, Settings settings) throws IOException { - Request request = new Request("PUT", "/" + name); - request.setJsonEntity("{\n \"settings\": " + Strings.toString(settings) + "}"); - client().performRequest(request); + createIndex(name, settings, null); } protected static void createIndex(String name, Settings settings, String mapping) throws IOException { - Request request = new Request("PUT", "/" + name); - request.setJsonEntity("{\n \"settings\": " + Strings.toString(settings) - + ", \"mappings\" : {" + mapping + "} }"); - client().performRequest(request); + createIndex(name, settings, mapping, null); } protected static void createIndex(String name, Settings settings, String mapping, String aliases) throws IOException { Request request = new Request("PUT", "/" + name); - request.setJsonEntity("{\n \"settings\": " + Strings.toString(settings) - + ", \"mappings\" : {" + mapping + "}" - + ", \"aliases\": {" + aliases + "} }"); + String entity = "{\"settings\": " + Strings.toString(settings); + if (mapping != null) { + entity += ",\"mappings\" : {" + mapping + "}"; + } + if (aliases != null) { + entity += ",\"aliases\": {" + aliases + "}"; + } + entity += "}"; + if (settings.getAsBoolean(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) == false) { + expectSoftDeletesWarning(request, name); + } + request.setJsonEntity(entity); client().performRequest(request); } @@ -999,6 +1004,19 @@ private static void updateIndexSettings(String index, Settings settings) throws client().performRequest(request); } + protected static void expectSoftDeletesWarning(Request request, String indexName) { + final List expectedWarnings = List.of( + "Creating indices with soft-deletes disabled is deprecated and will be removed in future Elasticsearch versions. " + + "Please do not specify value for setting [index.soft_deletes.enabled] of index [" + indexName + "]."); + if (nodeVersions.stream().allMatch(version -> version.onOrAfter(Version.V_8_0_0))) { + request.setOptions(RequestOptions.DEFAULT.toBuilder() + .setWarningsHandler(warnings -> warnings.equals(expectedWarnings) == false)); + } else if (nodeVersions.stream().anyMatch(version -> version.onOrAfter(Version.V_8_0_0))) { + request.setOptions(RequestOptions.DEFAULT.toBuilder() + .setWarningsHandler(warnings -> warnings.isEmpty() == false && warnings.equals(expectedWarnings) == false)); + } + } + protected static Map getIndexSettings(String index) throws IOException { Request request = new Request("GET", "/" + index + "/_settings"); request.addParameter("flat_settings", "true"); From 15f4be04e08626cace6dfa71cb4a7869ceff8f78 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Mon, 6 Jan 2020 15:45:20 +1100 Subject: [PATCH 383/686] Remove obsolete constructor from SSLService (#50347) This removes the old `SSLService(Settings, Environment)` constructor and converts all uses cases to the `SSLService(Environment)` constructor that was added in #49667 --- .../xpack/core/ssl/SSLService.java | 31 +++++---- .../transport/ProfileConfigurationsTests.java | 4 +- .../ssl/SSLConfigurationReloaderTests.java | 23 ++++--- .../xpack/core/ssl/SSLServiceTests.java | 60 ++++++++-------- .../xpack/core/ssl/TestsSSLService.java | 4 +- .../exporter/http/HttpExporterIT.java | 3 +- .../exporter/http/HttpExporterSslIT.java | 4 +- .../esnative/tool/CommandLineHttpClient.java | 8 +-- .../esnative/tool/SetupPasswordTool.java | 18 ++--- .../security/PkiRealmBootstrapCheckTests.java | 37 ++++------ .../xpack/security/SecurityTests.java | 2 +- ...ansportOpenIdConnectLogoutActionTests.java | 4 +- .../tool/CommandLineHttpClientTests.java | 10 +-- .../esnative/tool/SetupPasswordToolTests.java | 2 +- .../authc/ldap/ActiveDirectoryRealmTests.java | 4 +- .../security/authc/ldap/LdapRealmTests.java | 10 +-- .../authc/ldap/LdapSessionFactoryTests.java | 2 +- .../security/authc/ldap/LdapTestUtils.java | 2 +- .../LdapUserSearchSessionFactoryTests.java | 4 +- .../SessionFactoryLoadBalancingTests.java | 2 +- .../ldap/support/SessionFactoryTests.java | 14 ++-- .../oidc/OpenIdConnectAuthenticatorTests.java | 11 ++- .../security/authc/saml/SamlRealmTests.java | 5 +- ...stractSimpleSecurityTransportTestCase.java | 2 +- ...ecurityNetty4HttpServerTransportTests.java | 16 ++--- .../SecurityNioHttpServerTransportTests.java | 16 ++--- .../transport/ssl/SslIntegrationTests.java | 3 +- ...orMessageCertificateVerificationTests.java | 7 +- .../xpack/ssl/SSLErrorMessageFileTests.java | 6 +- .../xpack/ssl/SSLReloadIntegTests.java | 2 +- .../xpack/ssl/SSLTrustRestrictionsTests.java | 2 +- .../watcher/actions/email/EmailSslTests.java | 2 +- .../actions/webhook/WebhookActionTests.java | 2 +- .../webhook/WebhookHttpsIntegrationTests.java | 4 +- .../watcher/common/http/HttpClientTests.java | 68 +++++++++++-------- .../http/HttpConnectionTimeoutTests.java | 6 +- .../common/http/HttpReadTimeoutTests.java | 6 +- .../org/elasticsearch/test/OpenLdapTests.java | 2 +- ...OpenLdapUserSearchSessionFactoryTests.java | 2 +- .../ADLdapUserSearchSessionFactoryTests.java | 6 +- .../ldap/AbstractActiveDirectoryTestCase.java | 2 +- .../ActiveDirectorySessionFactoryTests.java | 4 +- 42 files changed, 216 insertions(+), 206 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java index a788090c831d9..2e7a3a5855943 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java @@ -100,6 +100,7 @@ public class SSLService { private static final Setting DIAGNOSE_TRUST_EXCEPTIONS_SETTING = Setting.boolSetting( "xpack.security.ssl.diagnose.trust", true, Setting.Property.NodeScope); + private final Environment env; private final Settings settings; private final boolean diagnoseTrustExceptions; @@ -120,33 +121,33 @@ public class SSLService { */ private final Map sslContexts; private final SetOnce transportSSLConfiguration = new SetOnce<>(); - private final Environment env; /** - * Create a new SSLService using the {@code Settings} from {@link Environment#settings()}. - * @see #SSLService(Settings, Environment) + * Create a new SSLService that parses the settings for the ssl contexts that need to be created, creates them, and then caches them + * for use later */ public SSLService(Environment environment) { - this(environment.settings(), environment); + this.env = environment; + this.settings = env.settings(); + this.diagnoseTrustExceptions = DIAGNOSE_TRUST_EXCEPTIONS_SETTING.get(environment.settings()); + this.sslConfigurations = new HashMap<>(); + this.sslContexts = loadSSLConfigurations(); } - /** - * Create a new SSLService that parses the settings for the ssl contexts that need to be created, creates them, and then caches them - * for use later - */ + @Deprecated public SSLService(Settings settings, Environment environment) { - this.settings = settings; this.env = environment; + this.settings = env.settings(); this.diagnoseTrustExceptions = DIAGNOSE_TRUST_EXCEPTIONS_SETTING.get(settings); this.sslConfigurations = new HashMap<>(); this.sslContexts = loadSSLConfigurations(); } - private SSLService(Settings settings, Environment environment, Map sslConfigurations, + private SSLService(Environment environment, Map sslConfigurations, Map sslContexts) { - this.settings = settings; this.env = environment; - this.diagnoseTrustExceptions = DIAGNOSE_TRUST_EXCEPTIONS_SETTING.get(settings); + this.settings = env.settings(); + this.diagnoseTrustExceptions = DIAGNOSE_TRUST_EXCEPTIONS_SETTING.get(environment.settings()); this.sslConfigurations = sslConfigurations; this.sslContexts = sslContexts; } @@ -157,7 +158,7 @@ private SSLService(Settings settings, Environment environment, Map loadSSLConfigurations() { @@ -489,9 +490,9 @@ X509ExtendedTrustManager wrapWithDiagnostics(X509ExtendedTrustManager trustManag * Parses the settings to load all SSLConfiguration objects that will be used. */ Map loadSSLConfigurations() { - Map sslContextHolders = new HashMap<>(); + final Map sslContextHolders = new HashMap<>(); - Map sslSettingsMap = new HashMap<>(); + final Map sslSettingsMap = new HashMap<>(); sslSettingsMap.put(XPackSettings.HTTP_SSL_PREFIX, getHttpTransportSSLSettings(settings)); sslSettingsMap.put("xpack.http.ssl", settings.getByPrefix("xpack.http.ssl.")); sslSettingsMap.putAll(getRealmsSSLSettings(settings)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/transport/ProfileConfigurationsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/transport/ProfileConfigurationsTests.java index fd7315d7457c2..8b3d4cc3bec75 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/transport/ProfileConfigurationsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/transport/ProfileConfigurationsTests.java @@ -30,7 +30,7 @@ public void testGetSecureTransportProfileConfigurations() { .put("transport.profiles.cert.xpack.security.ssl.verification_mode", VerificationMode.CERTIFICATE.name()) .build(); final Environment env = TestEnvironment.newEnvironment(settings); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(env); final SSLConfiguration defaultConfig = sslService.getSSLConfiguration("xpack.security.transport.ssl"); final Map profileConfigurations = ProfileConfigurations.get(settings, sslService, defaultConfig); assertThat(profileConfigurations.size(), Matchers.equalTo(3)); @@ -48,7 +48,7 @@ public void testGetInsecureTransportProfileConfigurations() { .put("transport.profiles.none.xpack.security.ssl.verification_mode", VerificationMode.NONE.name()) .build(); final Environment env = TestEnvironment.newEnvironment(settings); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(env); final SSLConfiguration defaultConfig = sslService.getSSLConfiguration("xpack.security.transport.ssl"); final Map profileConfigurations = ProfileConfigurations.get(settings, sslService, defaultConfig); assertThat(profileConfigurations.size(), Matchers.equalTo(2)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java index c2ba99a441616..2c9cc037e7cc4 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java @@ -147,7 +147,7 @@ public void testReloadingKeyStore() throws Exception { throw new RuntimeException("Exception starting or connecting to the mock server", e); } }; - validateSSLConfigurationIsReloaded(settings, env, keyMaterialPreChecks, modifier, keyMaterialPostChecks); + validateSSLConfigurationIsReloaded(env, keyMaterialPreChecks, modifier, keyMaterialPostChecks); } } /** @@ -174,7 +174,7 @@ public void testPEMKeyConfigReloading() throws Exception { .putList("xpack.security.transport.ssl.certificate_authorities", certPath.toString()) .setSecureSettings(secureSettings) .build(); - final Environment env = newEnvironment(); + final Environment env = TestEnvironment.newEnvironment(settings); // Load HTTPClient once. Client uses a keystore containing testnode key/cert as a truststore try (CloseableHttpClient client = getSSLClient(Collections.singletonList(certPath))) { final Consumer keyMaterialPreChecks = (context) -> { @@ -207,7 +207,7 @@ public void testPEMKeyConfigReloading() throws Exception { throw new RuntimeException("Exception starting or connecting to the mock server", e); } }; - validateSSLConfigurationIsReloaded(settings, env, keyMaterialPreChecks, modifier, keyMaterialPostChecks); + validateSSLConfigurationIsReloaded(env, keyMaterialPreChecks, modifier, keyMaterialPostChecks); } } @@ -259,7 +259,7 @@ public void testReloadingTrustStore() throws Exception { throw new RuntimeException("Error closing CloseableHttpClient", e); } }; - validateSSLConfigurationIsReloaded(settings, env, trustMaterialPreChecks, modifier, trustMaterialPostChecks); + validateSSLConfigurationIsReloaded(env, trustMaterialPreChecks, modifier, trustMaterialPostChecks); } } @@ -309,7 +309,7 @@ public void testReloadingPEMTrustConfig() throws Exception { throw new RuntimeException("Error closing CloseableHttpClient", e); } }; - validateSSLConfigurationIsReloaded(settings, env, trustMaterialPreChecks, modifier, trustMaterialPostChecks); + validateSSLConfigurationIsReloaded(env, trustMaterialPreChecks, modifier, trustMaterialPostChecks); } } @@ -331,7 +331,7 @@ public void testReloadingKeyStoreException() throws Exception { .put("path.home", createTempDir()) .build(); Environment env = TestEnvironment.newEnvironment(settings); - final SSLService sslService = new SSLService(settings, env); + final SSLService sslService = new SSLService(env); final SSLConfiguration config = sslService.getSSLConfiguration("xpack.security.transport.ssl."); final AtomicReference exceptionRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); @@ -353,6 +353,7 @@ void reloadSSLContext(SSLConfiguration configuration) { // truncate the keystore try (OutputStream ignore = Files.newOutputStream(keystorePath, StandardOpenOption.TRUNCATE_EXISTING)) { + // do nothing } latch.await(); @@ -384,7 +385,7 @@ public void testReloadingPEMKeyConfigException() throws Exception { .setSecureSettings(secureSettings) .build(); Environment env = TestEnvironment.newEnvironment(settings); - final SSLService sslService = new SSLService(settings, env); + final SSLService sslService = new SSLService(env); final SSLConfiguration config = sslService.getSSLConfiguration("xpack.security.transport.ssl."); final AtomicReference exceptionRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); @@ -430,7 +431,7 @@ public void testTrustStoreReloadException() throws Exception { .put("path.home", createTempDir()) .build(); Environment env = TestEnvironment.newEnvironment(settings); - final SSLService sslService = new SSLService(settings, env); + final SSLService sslService = new SSLService(env); final SSLConfiguration config = sslService.getSSLConfiguration("xpack.security.transport.ssl."); final AtomicReference exceptionRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); @@ -474,7 +475,7 @@ public void testPEMTrustReloadException() throws Exception { .put("path.home", createTempDir()) .build(); Environment env = TestEnvironment.newEnvironment(settings); - final SSLService sslService = new SSLService(settings, env); + final SSLService sslService = new SSLService(env); final SSLConfiguration config = sslService.sslConfiguration(settings.getByPrefix("xpack.security.transport.ssl.")); final AtomicReference exceptionRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); @@ -524,10 +525,10 @@ private Settings.Builder baseKeystoreSettings(Path tempDir, MockSecureSettings s .setSecureSettings(secureSettings); } - private void validateSSLConfigurationIsReloaded(Settings settings, Environment env, Consumer preChecks, + private void validateSSLConfigurationIsReloaded(Environment env, Consumer preChecks, Runnable modificationFunction, Consumer postChecks) throws Exception { final CountDownLatch reloadLatch = new CountDownLatch(1); - final SSLService sslService = new SSLService(settings, env); + final SSLService sslService = new SSLService(env); final SSLConfiguration config = sslService.getSSLConfiguration("xpack.security.transport.ssl"); new SSLConfigurationReloader(env, sslService, resourceWatcherService) { @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java index b2bf6974e319d..14654902b0405 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java @@ -121,7 +121,7 @@ public void testThatCustomTruststoreCanBeSpecified() throws Exception { .setSecureSettings(secureSettings) .put("transport.profiles.foo.xpack.security.ssl.truststore.path", testClientStore) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); MockSecureSettings secureCustomSettings = new MockSecureSettings(); secureCustomSettings.setString("truststore.secure_password", "testclient"); @@ -153,7 +153,7 @@ public void testThatSslContextCachingWorks() throws Exception { .put("xpack.security.transport.ssl.key", testnodeKey) .setSecureSettings(secureSettings) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); final Settings transportSSLSettings = settings.getByPrefix("xpack.security.transport.ssl."); SSLContext sslContext = sslService.sslContext(sslService.sslConfiguration(transportSSLSettings)); @@ -179,7 +179,7 @@ public void testThatKeyStoreAndKeyCanHaveDifferentPasswords() throws Exception { .setSecureSettings(secureSettings) .build(); - final SSLService sslService = new SSLService(settings, env); + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); SSLConfiguration configuration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); sslService.createSSLEngine(configuration, null, -1); } @@ -195,7 +195,7 @@ public void testIncorrectKeyPasswordThrowsException() throws Exception { .put("xpack.security.transport.ssl.keystore.path", differentPasswordsStore) .setSecureSettings(secureSettings) .build(); - final SSLService sslService = new SSLService(settings, env); + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); SSLConfiguration configuration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); sslService.createSSLEngine(configuration, null, -1); fail("expected an exception"); @@ -214,14 +214,14 @@ public void testThatSSLv3IsNotEnabled() throws Exception { .put("xpack.security.transport.ssl.key", testnodeKey) .setSecureSettings(secureSettings) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); SSLConfiguration configuration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); SSLEngine engine = sslService.createSSLEngine(configuration, null, -1); assertThat(Arrays.asList(engine.getEnabledProtocols()), not(hasItem("SSLv3"))); } public void testThatCreateClientSSLEngineWithoutAnySettingsWorks() throws Exception { - SSLService sslService = new SSLService(Settings.EMPTY, env); + SSLService sslService = new SSLService(env); SSLConfiguration configuration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); SSLEngine sslEngine = sslService.createSSLEngine(configuration, null, -1); assertThat(sslEngine, notNullValue()); @@ -235,7 +235,7 @@ public void testThatCreateSSLEngineWithOnlyTruststoreWorks() throws Exception { .put("xpack.http.ssl.truststore.path", testclientStore) .setSecureSettings(secureSettings) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); SSLConfiguration configuration = sslService.getSSLConfiguration("xpack.security.http.ssl"); SSLEngine sslEngine = sslService.createSSLEngine(configuration, null, -1); assertThat(sslEngine, notNullValue()); @@ -252,7 +252,7 @@ public void testCreateWithKeystoreIsValidForServer() throws Exception { .put("xpack.security.transport.ssl.keystore.type", testnodeStoreType) .setSecureSettings(secureSettings) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); assertTrue(sslService.isConfigurationValidForServerUsage(sslService.getSSLConfiguration("xpack.security.transport.ssl"))); } @@ -266,7 +266,7 @@ public void testValidForServer() throws Exception { .put("xpack.http.ssl.truststore.type", testnodeStoreType) .setSecureSettings(secureSettings) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); // Technically, we don't care whether xpack.http.ssl is valid for server - it's a client context, but we validate both of the // server contexts (http & transport) during construction, so this is the only way to make a non-server-valid context. assertFalse(sslService.isConfigurationValidForServerUsage(sslService.getSSLConfiguration("xpack.http.ssl"))); @@ -279,13 +279,13 @@ public void testValidForServer() throws Exception { .put("xpack.http.ssl.keystore.path", testnodeStore) .put("xpack.http.ssl.keystore.type", testnodeStoreType) .build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); assertTrue(sslService.isConfigurationValidForServerUsage(sslService.getSSLConfiguration("xpack.http.ssl"))); } public void testGetVerificationMode() throws Exception { assumeFalse("Can't run in a FIPS JVM, TrustAllConfig is not a SunJSSE TrustManagers", inFipsJvm()); - SSLService sslService = new SSLService(Settings.EMPTY, env); + SSLService sslService = new SSLService(env); assertThat(sslService.getSSLConfiguration("xpack.security.transport.ssl").verificationMode(), is(XPackSettings.VERIFICATION_MODE_DEFAULT)); @@ -294,14 +294,14 @@ public void testGetVerificationMode() throws Exception { .put("xpack.security.transport.ssl.verification_mode", "certificate") .put("transport.profiles.foo.xpack.security.ssl.verification_mode", "full") .build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); assertThat(sslService.getSSLConfiguration("xpack.security.transport.ssl.").verificationMode(), is(VerificationMode.CERTIFICATE)); assertThat(sslService.getSSLConfiguration("transport.profiles.foo.xpack.security.ssl.").verificationMode(), is(VerificationMode.FULL)); } public void testIsSSLClientAuthEnabled() throws Exception { - SSLService sslService = new SSLService(Settings.EMPTY, env); + SSLService sslService = new SSLService(env); assertTrue(sslService.getSSLConfiguration("xpack.security.transport.ssl").sslClientAuth().enabled()); Settings settings = Settings.builder() @@ -309,7 +309,7 @@ public void testIsSSLClientAuthEnabled() throws Exception { .put("xpack.security.transport.ssl.client_authentication", "optional") .put("transport.profiles.foo.port", "9400-9410") .build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); assertTrue(sslService.isSSLClientAuthEnabled(sslService.getSSLConfiguration("xpack.security.transport.ssl"))); assertTrue(sslService.isSSLClientAuthEnabled(sslService.getSSLConfiguration("transport.profiles.foo.xpack.security.ssl"))); } @@ -328,7 +328,7 @@ public void testThatHttpClientAuthDefaultsToNone() throws Exception { .put("xpack.security.transport.ssl.keystore.type", testnodeStoreType) .setSecureSettings(secureSettings) .build(); - final SSLService sslService = new SSLService(globalSettings, env); + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(globalSettings))); final SSLConfiguration globalConfig = sslService.getSSLConfiguration("xpack.security.transport.ssl"); assertThat(globalConfig.sslClientAuth(), is(SSLClientAuth.OPTIONAL)); @@ -348,7 +348,7 @@ public void testThatTruststorePasswordIsRequired() throws Exception { .put("xpack.security.transport.ssl.truststore.type", testnodeStoreType) .build(); ElasticsearchException e = - expectThrows(ElasticsearchException.class, () -> new SSLService(settings, env)); + expectThrows(ElasticsearchException.class, () -> new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings)))); assertThat(e, throwableWithMessage("failed to load SSL configuration [xpack.security.transport.ssl]")); assertThat(e.getCause(), throwableWithMessage(containsString("failed to initialize SSL TrustManager"))); } @@ -359,7 +359,7 @@ public void testThatKeystorePasswordIsRequired() throws Exception { .put("xpack.security.transport.ssl.keystore.type", testnodeStoreType) .build(); ElasticsearchException e = - expectThrows(ElasticsearchException.class, () -> new SSLService(settings, env)); + expectThrows(ElasticsearchException.class, () -> new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings)))); assertThat(e, throwableWithMessage("failed to load SSL configuration [xpack.security.transport.ssl]")); assertThat(e.getCause(), throwableWithMessage("failed to create trust manager")); } @@ -377,7 +377,7 @@ public void testCiphersAndInvalidCiphersWork() throws Exception { .setSecureSettings(secureSettings) .putList("xpack.security.transport.ssl.ciphers", ciphers.toArray(new String[ciphers.size()])) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); SSLConfiguration configuration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); SSLEngine engine = sslService.createSSLEngine(configuration, null, -1); assertThat(engine, is(notNullValue())); @@ -396,7 +396,7 @@ public void testInvalidCiphersOnlyThrowsException() throws Exception { .putList("xpack.security.transport.ssl.cipher_suites", new String[] { "foo", "bar" }) .build(); ElasticsearchException e = - expectThrows(ElasticsearchException.class, () -> new SSLService(settings, env)); + expectThrows(ElasticsearchException.class, () -> new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings)))); assertThat(e, throwableWithMessage("failed to load SSL configuration [xpack.security.transport.ssl]")); assertThat(e.getCause(), throwableWithMessage("none of the ciphers [foo, bar] are supported by this JVM")); } @@ -410,7 +410,7 @@ public void testThatSSLEngineHasCipherSuitesOrderSet() throws Exception { .put("xpack.security.transport.ssl.key", testnodeKey) .setSecureSettings(secureSettings) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); SSLConfiguration configuration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); SSLEngine engine = sslService.createSSLEngine(configuration, null, -1); assertThat(engine, is(notNullValue())); @@ -426,7 +426,7 @@ public void testThatSSLSocketFactoryHasProperCiphersAndProtocols() throws Except .put("xpack.security.transport.ssl.key", testnodeKey) .setSecureSettings(secureSettings) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); SSLConfiguration config = sslService.getSSLConfiguration("xpack.security.transport.ssl"); final SSLSocketFactory factory = sslService.sslSocketFactory(config); final String[] ciphers = sslService.supportedCiphers(factory.getSupportedCipherSuites(), config.cipherSuites(), false); @@ -452,7 +452,7 @@ public void testThatSSLEngineHasProperCiphersAndProtocols() throws Exception { .put("xpack.security.transport.ssl.key", testnodeKey) .setSecureSettings(secureSettings) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); SSLConfiguration configuration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); SSLEngine engine = sslService.createSSLEngine(configuration, null, -1); final String[] ciphers = sslService.supportedCiphers(engine.getSupportedCipherSuites(), configuration.cipherSuites(), false); @@ -542,7 +542,7 @@ public void testGetConfigurationByContextName() throws Exception { final Settings settings = builder .setSecureSettings(secureSettings) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); for (int i = 0; i < contextNames.length; i++) { final String name = contextNames[i]; @@ -576,7 +576,7 @@ public void testReadCertificateInformation() throws Exception { .setSecureSettings(secureSettings) .build(); - final SSLService sslService = new SSLService(settings, env); + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); final List certificates = new ArrayList<>(sslService.getLoadedCertificates()); assertThat(certificates, iterableWithSize(13)); Collections.sort(certificates, @@ -757,7 +757,7 @@ public int getSessionCacheSize() { @Network public void testThatSSLContextWithoutSettingsWorks() throws Exception { - SSLService sslService = new SSLService(Settings.EMPTY, env); + SSLService sslService = new SSLService(env); SSLContext sslContext = sslService.sslContext(sslService.sslConfiguration(Settings.EMPTY)); try (CloseableHttpClient client = HttpClients.custom().setSSLContext(sslContext).build()) { // Execute a GET on a site known to have a valid certificate signed by a trusted public CA @@ -775,7 +775,7 @@ public void testThatSSLContextTrustsJDKTrustedCAs() throws Exception { .put("xpack.security.transport.ssl.keystore.path", testclientStore) .setSecureSettings(secureSettings) .build(); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); SSLContext sslContext = sslService.sslContext(sslService.sslConfiguration(settings.getByPrefix("xpack.security.transport.ssl."))); try (CloseableHttpClient client = HttpClients.custom().setSSLContext(sslContext).build()) { // Execute a GET on a site known to have a valid certificate signed by a trusted public CA which will succeed because the JDK @@ -786,7 +786,7 @@ public void testThatSSLContextTrustsJDKTrustedCAs() throws Exception { @Network public void testThatSSLIOSessionStrategyWithoutSettingsWorks() throws Exception { - SSLService sslService = new SSLService(Settings.EMPTY, env); + SSLService sslService = new SSLService(env); SSLConfiguration sslConfiguration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); logger.info("SSL Configuration: {}", sslConfiguration); SSLIOSessionStrategy sslStrategy = sslService.sslIOSessionStrategy(sslConfiguration); @@ -808,7 +808,7 @@ public void testThatSSLIOSessionStrategyTrustsJDKTrustedCAs() throws Exception { .put("xpack.security.transport.ssl.keystore.path", testclientStore) .setSecureSettings(secureSettings) .build(); - final SSLService sslService = new SSLService(settings, env); + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); SSLIOSessionStrategy sslStrategy = sslService.sslIOSessionStrategy(sslService.getSSLConfiguration("xpack.security.transport.ssl")); try (CloseableHttpAsyncClient client = getAsyncHttpClient(sslStrategy)) { client.start(); @@ -824,7 +824,7 @@ public void testWrapTrustManagerWhenDiagnosticsEnabled() { if (randomBoolean()) { // randomly select between default, and explicit enabled builder.put("xpack.security.ssl.diagnose.trust", true); } - final SSLService sslService = new SSLService(builder.build(), env); + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(builder.build()))); final X509ExtendedTrustManager baseTrustManager = TrustAllConfig.INSTANCE.createTrustManager(env); final SSLConfiguration sslConfiguration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); final X509ExtendedTrustManager wrappedTrustManager = sslService.wrapWithDiagnostics(baseTrustManager, sslConfiguration); @@ -835,7 +835,7 @@ public void testWrapTrustManagerWhenDiagnosticsEnabled() { public void testDontWrapTrustManagerWhenDiagnosticsDisabled() { final Settings.Builder builder = Settings.builder(); builder.put("xpack.security.ssl.diagnose.trust", false); - final SSLService sslService = new SSLService(builder.build(), env); + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(builder.build()))); final X509ExtendedTrustManager baseTrustManager = TrustAllConfig.INSTANCE.createTrustManager(env); final SSLConfiguration sslConfiguration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); assertThat(sslService.wrapWithDiagnostics(baseTrustManager, sslConfiguration), sameInstance(baseTrustManager)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/TestsSSLService.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/TestsSSLService.java index e8766225a7a92..5a2c43d504616 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/TestsSSLService.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/TestsSSLService.java @@ -15,8 +15,8 @@ */ public class TestsSSLService extends SSLService { - public TestsSSLService(Settings settings, Environment environment) { - super(settings, environment); + public TestsSSLService(Environment environment) { + super(environment); } /** diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterIT.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterIT.java index 1bed5f1c7fff9..c27162d003ccf 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterIT.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterIT.java @@ -590,7 +590,8 @@ private HttpExporter createHttpExporter(final Settings settings) { final Exporter.Config config = new Exporter.Config("_http", "http", settings, clusterService(), new XPackLicenseState(Settings.EMPTY)); - return new HttpExporter(config, new SSLService(settings, environment), new ThreadContext(settings)); + final Environment env = TestEnvironment.newEnvironment(buildEnvSettings(settings)); + return new HttpExporter(config, new SSLService(env), new ThreadContext(settings)); } private void export(final Settings settings, final Collection docs) throws Exception { diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterSslIT.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterSslIT.java index fb0da753be3b5..94b1e3f4699b4 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterSslIT.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExporterSslIT.java @@ -12,7 +12,6 @@ import org.elasticsearch.bootstrap.JavaVersion; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.ESIntegTestCase.Scope; @@ -44,7 +43,6 @@ public class HttpExporterSslIT extends MonitoringIntegTestCase { private final Settings globalSettings = Settings.builder().put("path.home", createTempDir()).build(); - private final Environment environment = TestEnvironment.newEnvironment(globalSettings); private static MockWebServer webServer; private MockSecureSettings secureSettings; @@ -108,7 +106,7 @@ private MockWebServer buildWebServer() throws IOException { .put(globalSettings) .build(); - TestsSSLService sslService = new TestsSSLService(sslSettings, environment); + TestsSSLService sslService = new TestsSSLService(TestEnvironment.newEnvironment(sslSettings)); final SSLContext sslContext = sslService.sslContext("xpack.security.transport.ssl"); MockWebServer server = new MockWebServer(sslContext, false); server.start(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java index 7e9d5a8a32657..4b16e4d84bda3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java @@ -53,11 +53,9 @@ public class CommandLineHttpClient { */ private static final int READ_TIMEOUT = 35 * 1000; - private final Settings settings; private final Environment env; - public CommandLineHttpClient(Settings settings, Environment env) { - this.settings = settings; + public CommandLineHttpClient(Environment env) { this.env = env; } @@ -82,7 +80,7 @@ public HttpResponse execute(String method, URL url, String user, SecureString pa final HttpURLConnection conn; // If using SSL, need a custom service because it's likely a self-signed certificate if ("https".equalsIgnoreCase(url.getProtocol())) { - final SSLService sslService = new SSLService(settings, env); + final SSLService sslService = new SSLService(env); final HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection(); AccessController.doPrivileged((PrivilegedAction) () -> { final SSLConfiguration sslConfiguration = sslService.getHttpTransportSSLConfiguration(); @@ -133,6 +131,7 @@ public HttpResponse execute(String method, URL url, String user, SecureString pa } String getDefaultURL() { + final Settings settings = env.settings(); final String scheme = XPackSettings.HTTP_SSL_ENABLED.get(settings) ? "https" : "http"; List httpPublishHost = SETTING_HTTP_PUBLISH_HOST.get(settings); if (httpPublishHost.isEmpty()) { @@ -162,5 +161,4 @@ String getDefaultURL() { "provide the url", e); } } - } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java index 5ac81a0648019..9f853134d00b7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java @@ -50,7 +50,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.function.BiFunction; +import java.util.function.Function; import static java.util.Arrays.asList; @@ -68,15 +68,13 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { public static final List USERS = asList(ElasticUser.NAME, APMSystemUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME, RemoteMonitoringUser.NAME); - private final BiFunction clientFunction; + private final Function clientFunction; private final CheckedFunction keyStoreFunction; private CommandLineHttpClient client; SetupPasswordTool() { - this((environment, settings) -> { - return new CommandLineHttpClient(settings, environment); - }, (environment) -> { + this(environment -> new CommandLineHttpClient(environment), environment -> { KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.load(environment.configFile()); if (keyStoreWrapper == null) { throw new UserException(ExitCodes.CONFIG, @@ -86,8 +84,8 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { }); } - SetupPasswordTool(BiFunction clientFunction, - CheckedFunction keyStoreFunction) { + SetupPasswordTool(Function clientFunction, + CheckedFunction keyStoreFunction) { super("Sets the passwords for reserved users"); subcommands.put("auto", newAutoSetup()); subcommands.put("interactive", newInteractiveSetup()); @@ -261,12 +259,14 @@ void setupOptions(OptionSet options, Environment env) throws Exception { Settings settings = settingsBuilder.build(); elasticUserPassword = ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.get(settings); - client = clientFunction.apply(env, settings); + final Environment newEnv = new Environment(settings, env.configFile()); + Environment.assertEquivalent(newEnv, env); + + client = clientFunction.apply(newEnv); String providedUrl = urlOption.value(options); url = new URL(providedUrl == null ? client.getDefaultURL() : providedUrl); setShouldPrompt(options); - } private void setParser() { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheckTests.java index 62aece1f4fdf2..2d9411f5407b9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheckTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheckTests.java @@ -19,9 +19,8 @@ public class PkiRealmBootstrapCheckTests extends AbstractBootstrapCheckTestCase { public void testPkiRealmBootstrapDefault() throws Exception { - final Settings settings = Settings.EMPTY; - final Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); - assertFalse(runCheck(settings, env).isFailure()); + final Settings settings = Settings.builder().put("path.home", createTempDir()).build(); + assertFalse(runCheck(settings).isFailure()); } public void testBootstrapCheckWithPkiRealm() throws Exception { @@ -34,8 +33,7 @@ public void testBootstrapCheckWithPkiRealm() throws Exception { .put("path.home", createTempDir()) .setSecureSettings(secureSettings) .build(); - Environment env = TestEnvironment.newEnvironment(settings); - assertTrue(runCheck(settings, env).isFailure()); + assertTrue(runCheck(settings).isFailure()); // enable transport tls secureSettings.setString("xpack.security.transport.ssl.secure_key_passphrase", "testnode"); @@ -44,7 +42,7 @@ public void testBootstrapCheckWithPkiRealm() throws Exception { .put("xpack.security.transport.ssl.certificate", certPath) .put("xpack.security.transport.ssl.key", keyPath) .build(); - assertFalse(runCheck(settings, env).isFailure()); + assertFalse(runCheck(settings).isFailure()); // enable ssl for http secureSettings.setString("xpack.security.http.ssl.secure_key_passphrase", "testnode"); @@ -54,29 +52,25 @@ public void testBootstrapCheckWithPkiRealm() throws Exception { .put("xpack.security.http.ssl.certificate", certPath) .put("xpack.security.http.ssl.key", keyPath) .build(); - env = TestEnvironment.newEnvironment(settings); - assertTrue(runCheck(settings, env).isFailure()); + assertTrue(runCheck(settings).isFailure()); // enable client auth for http settings = Settings.builder().put(settings) .put("xpack.security.http.ssl.client_authentication", randomFrom("required", "optional")) .build(); - env = TestEnvironment.newEnvironment(settings); - assertFalse(runCheck(settings, env).isFailure()); + assertFalse(runCheck(settings).isFailure()); // disable http ssl settings = Settings.builder().put(settings) .put("xpack.security.http.ssl.enabled", false) .build(); - env = TestEnvironment.newEnvironment(settings); - assertTrue(runCheck(settings, env).isFailure()); + assertTrue(runCheck(settings).isFailure()); // set transport auth settings = Settings.builder().put(settings) .put("xpack.security.transport.client_authentication", randomFrom("required", "optional")) .build(); - env = TestEnvironment.newEnvironment(settings); - assertTrue(runCheck(settings, env).isFailure()); + assertTrue(runCheck(settings).isFailure()); // test with transport profile settings = Settings.builder().put(settings) @@ -84,12 +78,12 @@ public void testBootstrapCheckWithPkiRealm() throws Exception { .put("xpack.security.transport.client_authentication", "none") .put("transport.profiles.foo.xpack.security.ssl.client_authentication", randomFrom("required", "optional")) .build(); - env = TestEnvironment.newEnvironment(settings); - assertFalse(runCheck(settings, env).isFailure()); + assertFalse(runCheck(settings).isFailure()); } - private BootstrapCheck.BootstrapCheckResult runCheck(Settings settings, Environment env) throws Exception { - return new PkiRealmBootstrapCheck(new SSLService(settings, env)).check(createTestContext(settings, null)); + private BootstrapCheck.BootstrapCheckResult runCheck(Settings settings) throws Exception { + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(settings)); + return new PkiRealmBootstrapCheck(sslService).check(createTestContext(settings, null)); } public void testBootstrapCheckWithDisabledRealm() throws Exception { @@ -100,7 +94,7 @@ public void testBootstrapCheckWithDisabledRealm() throws Exception { .put("path.home", createTempDir()) .build(); Environment env = TestEnvironment.newEnvironment(settings); - assertFalse(runCheck(settings, env).isFailure()); + assertFalse(runCheck(settings).isFailure()); } public void testBootstrapCheckWithDelegationEnabled() throws Exception { @@ -119,8 +113,7 @@ public void testBootstrapCheckWithDelegationEnabled() throws Exception { .put("path.home", createTempDir()) .setSecureSettings(secureSettings) .build(); - Environment env = TestEnvironment.newEnvironment(settings); - assertFalse(runCheck(settings, env).isFailure()); + assertFalse(runCheck(settings).isFailure()); } public void testBootstrapCheckWithClosedSecuredSetting() throws Exception { @@ -140,7 +133,7 @@ public void testBootstrapCheckWithClosedSecuredSetting() throws Exception { .build(); Environment env = TestEnvironment.newEnvironment(settings); - final PkiRealmBootstrapCheck check = new PkiRealmBootstrapCheck(new SSLService(settings, env)); + final PkiRealmBootstrapCheck check = new PkiRealmBootstrapCheck(new SSLService(env)); secureSettings.close(); assertThat(check.check(createTestContext(settings, null)).isFailure(), Matchers.equalTo(expectFail)); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java index 2d663ea619f9a..d5b513284510f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java @@ -104,7 +104,7 @@ private Collection createComponents(Settings testSettings, SecurityExten .put("path.home", createTempDir()).build(); Environment env = TestEnvironment.newEnvironment(settings); licenseState = new TestUtils.UpdatableLicenseState(settings); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(env); security = new Security(settings, null, Arrays.asList(extensions)) { @Override protected XPackLicenseState getLicenseState() { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java index 387470090735b..d2ed6500036c4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java @@ -182,8 +182,8 @@ public void setup() throws Exception { final RealmConfig.RealmIdentifier realmIdentifier = new RealmConfig.RealmIdentifier("oidc", REALM_NAME); final RealmConfig realmConfig = new RealmConfig(realmIdentifier, settings, env, threadContext); - oidcRealm = new OpenIdConnectRealm(realmConfig, new SSLService(sslSettings, env), mock(UserRoleMapper.class), - mock(ResourceWatcherService.class)); + oidcRealm = new OpenIdConnectRealm(realmConfig, new SSLService(TestEnvironment.newEnvironment(sslSettings)), + mock(UserRoleMapper.class), mock(ResourceWatcherService.class)); when(realms.realm(realmConfig.name())).thenReturn(oidcRealm); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java index 4f2484d193116..65ae66278cee2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java @@ -8,7 +8,6 @@ import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.http.MockResponse; @@ -36,7 +35,6 @@ public class CommandLineHttpClientTests extends ESTestCase { private MockWebServer webServer; - private Environment environment = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); private Path certPath; private Path keyPath; @@ -60,7 +58,7 @@ public void testCommandLineHttpClientCanExecuteAndReturnCorrectResultUsingSSLSet .put("xpack.security.http.ssl.certificate_authorities", certPath.toString()) .put("xpack.security.http.ssl.verification_mode", VerificationMode.CERTIFICATE) .build(); - CommandLineHttpClient client = new CommandLineHttpClient(settings, environment); + CommandLineHttpClient client = new CommandLineHttpClient(TestEnvironment.newEnvironment(settings)); HttpResponse httpResponse = client.execute("GET", new URL("https://localhost:" + webServer.getPort() + "/test"), "u1", new SecureString(new char[]{'p'}), () -> null, is -> responseBuilder(is)); @@ -71,16 +69,17 @@ public void testCommandLineHttpClientCanExecuteAndReturnCorrectResultUsingSSLSet public void testGetDefaultURLFailsWithHelpfulMessage() { Settings settings = Settings.builder() + .put("path.home", createTempDir()) .put("network.host", "_ec2:privateIpv4_") .build(); - CommandLineHttpClient client = new CommandLineHttpClient(settings, environment); + CommandLineHttpClient client = new CommandLineHttpClient(TestEnvironment.newEnvironment(settings)); assertThat(expectThrows(IllegalStateException.class, () -> client.getDefaultURL()).getMessage(), containsString("unable to determine default URL from settings, please use the -u option to explicitly provide the url")); } private MockWebServer createMockWebServer() { Settings settings = getHttpSslSettings().build(); - TestsSSLService sslService = new TestsSSLService(settings, environment); + TestsSSLService sslService = new TestsSSLService(TestEnvironment.newEnvironment(settings)); return new MockWebServer(sslService.sslContext("xpack.security.http.ssl."), false); } @@ -88,6 +87,7 @@ private Settings.Builder getHttpSslSettings() { MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("xpack.security.http.ssl.secure_key_passphrase", "testnode"); return Settings.builder() + .put("path.home", createTempDir()) .put("xpack.security.http.ssl.enabled", true) .put("xpack.security.http.ssl.key", keyPath.toString()) .put("xpack.security.http.ssl.certificate", certPath.toString()) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java index adb4fc58d3ed4..e80c4636e9766 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java @@ -124,7 +124,7 @@ public void setSecretsAndKeyStore() throws Exception { @Override protected Command newCommand() { - return new SetupPasswordTool((e, s) -> httpClient, (e) -> keyStore) { + return new SetupPasswordTool(env -> httpClient, env -> keyStore) { @Override protected AutoSetup newAutoSetup() { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java index 4080b318a2eeb..fea74c5d660be 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java @@ -143,7 +143,7 @@ public void start() throws Exception { threadPool = new TestThreadPool("active directory realm tests"); resourceWatcherService = new ResourceWatcherService(Settings.EMPTY, threadPool); globalSettings = Settings.builder().put("path.home", createTempDir()).build(); - sslService = new SSLService(globalSettings, TestEnvironment.newEnvironment(globalSettings)); + sslService = new SSLService(TestEnvironment.newEnvironment(globalSettings)); licenseState = new TestUtils.UpdatableLicenseState(); } @@ -168,7 +168,7 @@ public boolean enableWarningsCheck() { private RealmConfig setupRealm(RealmConfig.RealmIdentifier realmIdentifier, Settings localSettings) { final Settings mergedSettings = Settings.builder().put(globalSettings).put(localSettings).build(); final Environment env = TestEnvironment.newEnvironment(mergedSettings); - this.sslService = new SSLService(mergedSettings, env); + this.sslService = new SSLService(env); return new RealmConfig( realmIdentifier, mergedSettings, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java index a56c5550c65c2..39268be35a8db 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java @@ -99,7 +99,7 @@ public void init() throws Exception { threadPool = new TestThreadPool("ldap realm tests"); resourceWatcherService = new ResourceWatcherService(Settings.EMPTY, threadPool); defaultGlobalSettings = Settings.builder().put("path.home", createTempDir()).build(); - sslService = new SSLService(defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings)); + sslService = new SSLService(TestEnvironment.newEnvironment(defaultGlobalSettings)); licenseState = mock(XPackLicenseState.class); when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true); } @@ -305,6 +305,7 @@ public void testLdapRealmSelectsLdapSessionFactory() throws Exception { String userTemplate = VALID_USER_TEMPLATE; Settings settings = Settings.builder() .put(defaultGlobalSettings) + .putList(getFullSettingKey(identifier, URLS_SETTING), ldapUrls()) .putList(getFullSettingKey(identifier.getName(), LdapSessionFactorySettings.USER_DN_TEMPLATES_SETTING), userTemplate) .put(getFullSettingKey(identifier, SearchGroupsResolverSettings.BASE_DN), groupSearchBase) @@ -312,7 +313,8 @@ public void testLdapRealmSelectsLdapSessionFactory() throws Exception { .put(getFullSettingKey(identifier, SSLConfigurationSettings.VERIFICATION_MODE_SETTING_REALM), VerificationMode.CERTIFICATE) .build(); RealmConfig config = getRealmConfig(identifier, settings); - SessionFactory sessionFactory = LdapRealm.sessionFactory(config, new SSLService(settings, config.env()), threadPool); + final SSLService ssl = new SSLService(config.env()); + SessionFactory sessionFactory = LdapRealm.sessionFactory(config, ssl, threadPool); assertThat(sessionFactory, is(instanceOf(LdapSessionFactory.class))); } @@ -332,7 +334,7 @@ public void testLdapRealmSelectsLdapUserSearchSessionFactory() throws Exception .put(getFullSettingKey(identifier, SSLConfigurationSettings.VERIFICATION_MODE_SETTING_REALM), VerificationMode.CERTIFICATE) .build(); final RealmConfig config = getRealmConfig(identifier, settings); - SessionFactory sessionFactory = LdapRealm.sessionFactory(config, new SSLService(config.settings(), config.env()), threadPool); + SessionFactory sessionFactory = LdapRealm.sessionFactory(config, new SSLService(config.env()), threadPool); try { assertThat(sessionFactory, is(instanceOf(LdapUserSearchSessionFactory.class))); } finally { @@ -530,7 +532,7 @@ public void testUsageStats() throws Exception { RealmConfig config = getRealmConfig(identifier, settings.build()); - LdapSessionFactory ldapFactory = new LdapSessionFactory(config, new SSLService(config.settings(), config.env()), threadPool); + LdapSessionFactory ldapFactory = new LdapSessionFactory(config, new SSLService(config.env()), threadPool); LdapRealm realm = new LdapRealm(config, ldapFactory, new DnRoleMapper(config, resourceWatcherService), threadPool); realm.initialize(Collections.singleton(realm), licenseState); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapSessionFactoryTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapSessionFactoryTests.java index f7f38d41a2d66..3ba6d0da34824 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapSessionFactoryTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapSessionFactoryTests.java @@ -63,7 +63,7 @@ public void setup() throws Exception { .put("path.home", createTempDir()) .putList(RealmSettings.realmSslPrefix(REALM_IDENTIFIER) + "certificate_authorities", ldapCaPath.toString()) .build(); - sslService = new SSLService(globalSettings, TestEnvironment.newEnvironment(globalSettings)); + sslService = new SSLService(TestEnvironment.newEnvironment(globalSettings)); threadPool = new TestThreadPool("LdapSessionFactoryTests thread pool"); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapTestUtils.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapTestUtils.java index 65eb36aeba73b..f8d1281434567 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapTestUtils.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapTestUtils.java @@ -40,7 +40,7 @@ public static LDAPConnection openConnection(String url, String bindDN, String bi secureSettings.setString("xpack.security.authc.realms.ldap.bar.ssl.truststore.secure_password", "changeit"); Settings settings = builder.build(); Environment env = TestEnvironment.newEnvironment(settings); - SSLService sslService = new SSLService(settings, env); + SSLService sslService = new SSLService(env); LDAPURL ldapurl = new LDAPURL(url); LDAPConnectionOptions options = new LDAPConnectionOptions(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java index d6a370d9d3464..b59d95cb7a9da 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java @@ -21,7 +21,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; @@ -56,7 +55,6 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { @Before public void init() throws Exception { Path certPath = getDataPath("support/smb_ca.crt"); - Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); /* * Prior to each test we reinitialize the socket factory with a new SSLService so that we get a new SSLContext. * If we re-use an SSLContext, previously connected sessions can get re-established which breaks hostname @@ -68,7 +66,7 @@ public void init() throws Exception { .put("xpack.security.transport.ssl.enabled", false) .put("xpack.security.transport.ssl.certificate_authorities", certPath) .build(); - sslService = new SSLService(globalSettings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(globalSettings)); threadPool = new TestThreadPool("LdapUserSearchSessionFactoryTests"); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactoryLoadBalancingTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactoryLoadBalancingTests.java index 7c80de3ace4fe..f0ace87cd6990 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactoryLoadBalancingTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactoryLoadBalancingTests.java @@ -292,7 +292,7 @@ private TestSessionFactory createSessionFactory(LdapLoadBalancing loadBalancing) Settings globalSettings = Settings.builder().put("path.home", createTempDir()).put(settings).build(); RealmConfig config = new RealmConfig(REALM_IDENTIFIER, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(Settings.EMPTY)); - return new TestSessionFactory(config, new SSLService(Settings.EMPTY, TestEnvironment.newEnvironment(config.settings())), + return new TestSessionFactory(config, new SSLService(TestEnvironment.newEnvironment(config.settings())), threadPool); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactoryTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactoryTests.java index bb93e95950e86..313cd943f19a3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactoryTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactoryTests.java @@ -52,7 +52,7 @@ public void testConnectionFactoryReturnsCorrectLDAPConnectionOptionsWithDefaultS final Environment environment = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); RealmConfig realmConfig = new RealmConfig(new RealmConfig.RealmIdentifier("ldap", "conn_settings"), environment.settings(), environment, new ThreadContext(Settings.EMPTY)); - LDAPConnectionOptions options = SessionFactory.connectionOptions(realmConfig, new SSLService(environment.settings(), environment), + LDAPConnectionOptions options = SessionFactory.connectionOptions(realmConfig, new SSLService(environment), logger); assertThat(options.followReferrals(), is(equalTo(true))); assertThat(options.allowConcurrentSocketFactoryUse(), is(equalTo(true))); @@ -72,9 +72,9 @@ public void testConnectionFactoryReturnsCorrectLDAPConnectionOptions() throws Ex .put("path.home", pathHome) .build(); - final Environment environment = TestEnvironment.newEnvironment(settings); + Environment environment = TestEnvironment.newEnvironment(settings); RealmConfig realmConfig = new RealmConfig(realmId, settings, environment, new ThreadContext(settings)); - LDAPConnectionOptions options = SessionFactory.connectionOptions(realmConfig, new SSLService(settings, environment), logger); + LDAPConnectionOptions options = SessionFactory.connectionOptions(realmConfig, new SSLService(environment), logger); assertThat(options.followReferrals(), is(equalTo(false))); assertThat(options.allowConcurrentSocketFactoryUse(), is(equalTo(true))); assertThat(options.getConnectTimeoutMillis(), is(equalTo(10))); @@ -88,7 +88,7 @@ public void testConnectionFactoryReturnsCorrectLDAPConnectionOptions() throws Ex .put("path.home", pathHome) .build(); realmConfig = new RealmConfig(realmId, settings, environment, new ThreadContext(settings)); - options = SessionFactory.connectionOptions(realmConfig, new SSLService(settings, environment), logger); + options = SessionFactory.connectionOptions(realmConfig, new SSLService(TestEnvironment.newEnvironment(settings)), logger); assertThat(options.getSSLSocketVerifier(), is(instanceOf(TrustAllSSLSocketVerifier.class))); // Can't run in FIPS with verification_mode none, disable this check instead of duplicating the test case @@ -97,8 +97,9 @@ public void testConnectionFactoryReturnsCorrectLDAPConnectionOptions() throws Ex .put(getFullSettingKey(realmId, SSLConfigurationSettings.VERIFICATION_MODE_SETTING_REALM), VerificationMode.NONE) .put("path.home", pathHome) .build(); + environment = TestEnvironment.newEnvironment(settings); realmConfig = new RealmConfig(realmId, settings, environment, new ThreadContext(settings)); - options = SessionFactory.connectionOptions(realmConfig, new SSLService(settings, environment), logger); + options = SessionFactory.connectionOptions(realmConfig, new SSLService(environment), logger); assertThat(options.getSSLSocketVerifier(), is(instanceOf(TrustAllSSLSocketVerifier.class))); } @@ -106,8 +107,9 @@ public void testConnectionFactoryReturnsCorrectLDAPConnectionOptions() throws Ex .put(getFullSettingKey(realmId, SSLConfigurationSettings.VERIFICATION_MODE_SETTING_REALM), VerificationMode.FULL) .put("path.home", pathHome) .build(); + environment = TestEnvironment.newEnvironment(settings); realmConfig = new RealmConfig(realmId, settings, environment, new ThreadContext(settings)); - options = SessionFactory.connectionOptions(realmConfig, new SSLService(settings, environment), logger); + options = SessionFactory.connectionOptions(realmConfig, new SSLService(environment), logger); assertThat(options.getSSLSocketVerifier(), is(instanceOf(HostNameSSLSocketVerifier.class))); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java index ad4b9fcd2af81..bf0bdbee13e9c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java @@ -86,14 +86,13 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase { private OpenIdConnectAuthenticator authenticator; - private Settings globalSettings; private Environment env; private ThreadContext threadContext; private int callsToReloadJwk; @Before public void setup() { - globalSettings = Settings.builder().put("path.home", createTempDir()) + final Settings globalSettings = Settings.builder().put("path.home", createTempDir()) .put("xpack.security.authc.realms.oidc.oidc-realm.ssl.verification_mode", "certificate").build(); env = TestEnvironment.newEnvironment(globalSettings); threadContext = new ThreadContext(globalSettings); @@ -109,7 +108,7 @@ public void cleanup() { private OpenIdConnectAuthenticator buildAuthenticator() throws URISyntaxException { final RealmConfig config = buildConfig(getBasicRealmSettings().build(), threadContext); - return new OpenIdConnectAuthenticator(config, getOpConfig(), getDefaultRpConfig(), new SSLService(globalSettings, env), null); + return new OpenIdConnectAuthenticator(config, getOpConfig(), getDefaultRpConfig(), new SSLService(env), null); } private OpenIdConnectAuthenticator buildAuthenticator(OpenIdConnectProviderConfiguration opConfig, RelyingPartyConfiguration rpConfig, @@ -117,7 +116,7 @@ private OpenIdConnectAuthenticator buildAuthenticator(OpenIdConnectProviderConfi final RealmConfig config = buildConfig(getBasicRealmSettings().build(), threadContext); final JWSVerificationKeySelector keySelector = new JWSVerificationKeySelector(rpConfig.getSignatureAlgorithm(), jwkSource); final IDTokenValidator validator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), keySelector, null); - return new OpenIdConnectAuthenticator(config, opConfig, rpConfig, new SSLService(globalSettings, env), validator, + return new OpenIdConnectAuthenticator(config, opConfig, rpConfig, new SSLService(env), validator, null); } @@ -126,7 +125,7 @@ private OpenIdConnectAuthenticator buildAuthenticator(OpenIdConnectProviderConfi final RealmConfig config = buildConfig(getBasicRealmSettings().build(), threadContext); final IDTokenValidator validator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), rpConfig.getSignatureAlgorithm(), new Secret(rpConfig.getClientSecret().toString())); - return new OpenIdConnectAuthenticator(config, opConfig, rpConfig, new SSLService(globalSettings, env), validator, + return new OpenIdConnectAuthenticator(config, opConfig, rpConfig, new SSLService(env), validator, null); } @@ -984,7 +983,7 @@ private Tuple getRandomJwkForType(String type) throws Exception { } else { throw new IllegalArgumentException("Invalid key type :" + type); } - return new Tuple(key, new JWKSet(jwk)); + return new Tuple<>(key, new JWKSet(jwk)); } private Curve curveFromHashSize(int size) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java index b4800c798a7f5..9b86075107876 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java @@ -141,7 +141,7 @@ public void testReadIdpMetadataFromHttps() throws Exception { .put("path.home", createTempDir()) .setSecureSettings(mockSecureSettings) .build(); - TestsSSLService sslService = new TestsSSLService(settings, TestEnvironment.newEnvironment(settings)); + TestsSSLService sslService = new TestsSSLService(TestEnvironment.newEnvironment(settings)); try (MockWebServer proxyServer = new MockWebServer(sslService.sslContext("xpack.security.http.ssl"), false)) { proxyServer.start(); @@ -690,9 +690,8 @@ private EntityDescriptor mockIdp() { private Tuple buildConfig(String idpMetaDataPath) throws Exception { Settings globalSettings = buildSettings(idpMetaDataPath).build(); - final Environment env = TestEnvironment.newEnvironment(globalSettings); final RealmConfig config = realmConfigFromGlobalSettings(globalSettings); - final SSLService sslService = new SSLService(globalSettings, env); + final SSLService sslService = new SSLService(config.env()); return new Tuple<>(config, sslService); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/AbstractSimpleSecurityTransportTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/AbstractSimpleSecurityTransportTestCase.java index ffb470083e90e..1dd7981af5038 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/AbstractSimpleSecurityTransportTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/AbstractSimpleSecurityTransportTestCase.java @@ -90,7 +90,7 @@ protected SSLService createSSLService(Settings settings) { .setSecureSettings(secureSettings) .build(); try { - return new SSLService(settings1, TestEnvironment.newEnvironment(settings1)); + return new SSLService(TestEnvironment.newEnvironment(settings1)); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java index 8efa61ebe40b6..e73a4ad715109 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java @@ -55,14 +55,14 @@ public void createSSLService() { .setSecureSettings(secureSettings) .build(); env = TestEnvironment.newEnvironment(settings); - sslService = new SSLService(settings, env); + sslService = new SSLService(env); } public void testDefaultClientAuth() throws Exception { Settings settings = Settings.builder() .put(env.settings()) .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true).build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher()); @@ -78,7 +78,7 @@ public void testOptionalClientAuth() throws Exception { .put(env.settings()) .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) .put("xpack.security.http.ssl.client_authentication", value).build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher()); @@ -94,7 +94,7 @@ public void testRequiredClientAuth() throws Exception { .put(env.settings()) .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) .put("xpack.security.http.ssl.client_authentication", value).build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher()); @@ -110,7 +110,7 @@ public void testNoClientAuth() throws Exception { .put(env.settings()) .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) .put("xpack.security.http.ssl.client_authentication", value).build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher()); @@ -124,7 +124,7 @@ public void testCustomSSLConfiguration() throws Exception { Settings settings = Settings.builder() .put(env.settings()) .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true).build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher()); @@ -137,7 +137,7 @@ public void testCustomSSLConfiguration() throws Exception { .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) .put("xpack.security.http.ssl.supported_protocols", "TLSv1.2") .build(); - sslService = new SSLService(settings, TestEnvironment.newEnvironment(settings)); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher()); handler = transport.configureServerChannelHandler(); @@ -158,7 +158,7 @@ public void testNoExceptionWhenConfiguredWithoutSslKeySSLDisabled() throws Excep .put("path.home", createTempDir()) .build(); env = TestEnvironment.newEnvironment(settings); - sslService = new SSLService(settings, env); + sslService = new SSLService(env); SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java index 14addd0620b43..f1474ccc77145 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java @@ -63,7 +63,7 @@ public void createSSLService() { .setSecureSettings(secureSettings) .build(); env = TestEnvironment.newEnvironment(settings); - sslService = new SSLService(settings, env); + sslService = new SSLService(env); } public void testDefaultClientAuth() throws IOException { @@ -71,7 +71,7 @@ public void testDefaultClientAuth() throws IOException { .put(env.settings()) .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true).build(); nioGroupFactory = new NioGroupFactory(settings, logger); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService, nioGroupFactory); @@ -91,7 +91,7 @@ public void testOptionalClientAuth() throws IOException { .put(env.settings()) .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) .put("xpack.security.http.ssl.client_authentication", value).build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); nioGroupFactory = new NioGroupFactory(settings, logger); SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), @@ -113,7 +113,7 @@ public void testRequiredClientAuth() throws IOException { .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) .put("xpack.security.http.ssl.client_authentication", value).build(); nioGroupFactory = new NioGroupFactory(settings, logger); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService, nioGroupFactory); @@ -133,7 +133,7 @@ public void testNoClientAuth() throws IOException { .put(env.settings()) .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) .put("xpack.security.http.ssl.client_authentication", value).build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); nioGroupFactory = new NioGroupFactory(settings, logger); SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), @@ -152,7 +152,7 @@ public void testCustomSSLConfiguration() throws IOException { Settings settings = Settings.builder() .put(env.settings()) .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true).build(); - sslService = new SSLService(settings, env); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); nioGroupFactory = new NioGroupFactory(settings, logger); SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), @@ -168,7 +168,7 @@ public void testCustomSSLConfiguration() throws IOException { .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) .put("xpack.security.http.ssl.supported_protocols", "TLSv1.2") .build(); - sslService = new SSLService(settings, TestEnvironment.newEnvironment(settings)); + sslService = new SSLService(TestEnvironment.newEnvironment(settings)); nioGroupFactory = new NioGroupFactory(settings, logger); transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), @@ -191,7 +191,7 @@ public void testNoExceptionWhenConfiguredWithoutSslKeySSLDisabled() { .put("path.home", createTempDir()) .build(); env = TestEnvironment.newEnvironment(settings); - sslService = new SSLService(settings, env); + sslService = new SSLService(env); nioGroupFactory = new NioGroupFactory(settings, logger); SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ssl/SslIntegrationTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ssl/SslIntegrationTests.java index 741b70c2258c3..7d24b273de209 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ssl/SslIntegrationTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ssl/SslIntegrationTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.xpack.core.common.socket.SocketAccess; @@ -68,7 +69,7 @@ public void testThatConnectionToHTTPWorks() throws Exception { "/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testclient.crt", "xpack.security.http.", Arrays.asList("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); - SSLService service = new SSLService(builder.build(), newEnvironment()); + SSLService service = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(builder.build()))); CredentialsProvider provider = new BasicCredentialsProvider(); provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(nodeClientUsername(), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLErrorMessageCertificateVerificationTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLErrorMessageCertificateVerificationTests.java index 691b6501273a5..2c21b3984309a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLErrorMessageCertificateVerificationTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLErrorMessageCertificateVerificationTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.DiagnosticTrustManager; +import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.MockLogAppender; import org.elasticsearch.test.http.MockResponse; @@ -58,7 +59,7 @@ public void testMessageForHttpClientHostnameVerificationFailure() throws IOExcep SSLClientAuth.NONE, VerificationMode.FULL, null) .putList("xpack.http.ssl.certificate_authorities", getPath("ca1.crt")) .build(); - final SSLService sslService = new SSLService(sslSetup, newEnvironment()); + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(sslSetup))); try (MockWebServer webServer = initWebServer(sslService); CloseableHttpClient client = buildHttpClient(sslService)) { final HttpGet request = new HttpGet(webServer.getUri("/")); @@ -79,7 +80,7 @@ public void testMessageForRestClientHostnameVerificationFailure() throws IOExcep // Client .putList("xpack.http.ssl.certificate_authorities", getPath("ca1.crt")) .build(); - final SSLService sslService = new SSLService(sslSetup, newEnvironment()); + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(sslSetup))); try (MockWebServer webServer = initWebServer(sslService)) { try (RestClient restClient = buildRestClient(sslService, webServer)) { restClient.performRequest(new Request("GET", "/")); @@ -98,7 +99,7 @@ public void testDiagnosticTrustManagerForHostnameVerificationFailure() throws Ex SSLClientAuth.NONE, VerificationMode.FULL, null) .putList("xpack.http.ssl.certificate_authorities", getPath("ca1.crt")) .build(); - final SSLService sslService = new SSLService(settings, newEnvironment()); + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings))); final SSLConfiguration clientSslConfig = sslService.getSSLConfiguration(HTTP_CLIENT_SSL); final SSLSocketFactory clientSocketFactory = sslService.sslSocketFactory(clientSslConfig); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLErrorMessageFileTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLErrorMessageFileTests.java index de12f2c3cf331..5efce0a8ab0fa 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLErrorMessageFileTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLErrorMessageFileTests.java @@ -346,10 +346,12 @@ private Settings.Builder configureWorkingKeystore(String prefix, Settings.Builde } private ElasticsearchException expectFailure(Settings.Builder settings) { - return expectThrows(ElasticsearchException.class, () -> new SSLService(settings.build(), env)); + return expectThrows(ElasticsearchException.class, + () -> new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings.build())))); } + private SSLService expectSuccess(Settings.Builder settings) { - return new SSLService(settings.build(), env); + return new SSLService(TestEnvironment.newEnvironment(buildEnvSettings(settings.build()))); } private String resource(String fileName) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLReloadIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLReloadIntegTests.java index 6354ddb5046c3..6f51f453f9178 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLReloadIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLReloadIntegTests.java @@ -108,7 +108,7 @@ public void testThatSSLConfigurationReloadsOnModification() throws Exception { .setSecureSettings(secureSettings) .build(); String node = randomFrom(internalCluster().getNodeNames()); - SSLService sslService = new SSLService(settings, TestEnvironment.newEnvironment(settings)); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(settings)); SSLConfiguration sslConfiguration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); SSLSocketFactory sslSocketFactory = sslService.sslSocketFactory(sslConfiguration); TransportAddress address = internalCluster() diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLTrustRestrictionsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLTrustRestrictionsTests.java index 2f097480f928d..e23ed8dcb20a3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLTrustRestrictionsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLTrustRestrictionsTests.java @@ -221,7 +221,7 @@ private void tryConnect(CertificateInfo certificate, boolean shouldFail) throws .build(); String node = randomFrom(internalCluster().getNodeNames()); - SSLService sslService = new SSLService(settings, TestEnvironment.newEnvironment(settings)); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(settings)); SSLConfiguration sslConfiguration = sslService.getSSLConfiguration("xpack.security.transport.ssl"); SSLSocketFactory sslSocketFactory = sslService.sslSocketFactory(sslConfiguration); TransportAddress address = internalCluster().getInstance(Transport.class, node).boundAddress().publishAddress(); diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/email/EmailSslTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/email/EmailSslTests.java index 70d7f2f6dd5ee..91bf327a37e18 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/email/EmailSslTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/email/EmailSslTests.java @@ -138,7 +138,7 @@ private ExecutableEmailAction buildEmailAction(Settings.Builder baseSettings, Mo Set> registeredSettings = new HashSet<>(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); registeredSettings.addAll(EmailService.getSettings()); ClusterSettings clusterSettings = new ClusterSettings(settings, registeredSettings); - SSLService sslService = new SSLService(settings, TestEnvironment.newEnvironment(settings)); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(settings)); final EmailService emailService = new EmailService(settings, null, sslService, clusterSettings); EmailTemplate emailTemplate = EmailTemplate.builder().from("from@example.org").to("to@example.org") .subject("subject").textBody("body").build(); diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/webhook/WebhookActionTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/webhook/WebhookActionTests.java index 439eb45f0159f..7cb87c62a2a3a 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/webhook/WebhookActionTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/webhook/WebhookActionTests.java @@ -215,7 +215,7 @@ private WebhookActionFactory webhookFactory(HttpClient client) { public void testThatSelectingProxyWorks() throws Exception { Environment environment = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); - try (HttpClient httpClient = new HttpClient(Settings.EMPTY, new SSLService(environment.settings(), environment), null, + try (HttpClient httpClient = new HttpClient(Settings.EMPTY, new SSLService(environment), null, mockClusterService()); MockWebServer proxyServer = new MockWebServer()) { proxyServer.start(); diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/webhook/WebhookHttpsIntegrationTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/webhook/WebhookHttpsIntegrationTests.java index c03d924cd6faa..1deced67b3d23 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/webhook/WebhookHttpsIntegrationTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/webhook/WebhookHttpsIntegrationTests.java @@ -64,8 +64,8 @@ protected Settings nodeSettings(int nodeOrdinal) { @Before public void startWebservice() throws Exception { - Settings settings = getInstanceFromMaster(Settings.class); - TestsSSLService sslService = new TestsSSLService(settings, getInstanceFromMaster(Environment.class)); + final Environment environment = getInstanceFromMaster(Environment.class); + final TestsSSLService sslService = new TestsSSLService(environment); webServer = new MockWebServer(sslService.sslContext("xpack.http.ssl"), false); webServer.start(); } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpClientTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpClientTests.java index e83f9154c5459..f820b530de26d 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpClientTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpClientTests.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.watcher.common.http; -import com.carrotsearch.randomizedtesting.generators.RandomStrings; import com.sun.net.httpserver.HttpsServer; import org.apache.http.HttpHeaders; import org.apache.http.HttpHost; @@ -86,7 +85,7 @@ public void init() throws Exception { ClusterService clusterService = mock(ClusterService.class); ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(HttpSettings.getSettings())); when(clusterService.getClusterSettings()).thenReturn(clusterSettings); - httpClient = new HttpClient(Settings.EMPTY, new SSLService(environment.settings(), environment), null, clusterService); + httpClient = new HttpClient(Settings.EMPTY, new SSLService(environment), null, clusterService); } @After @@ -189,14 +188,17 @@ public void testHttps() throws Exception { Path keyPath = getDataPath("/org/elasticsearch/xpack/security/keystore/testnode.pem"); MockSecureSettings secureSettings = new MockSecureSettings(); Settings settings = Settings.builder() + .put(environment.settings()) .put("xpack.http.ssl.certificate_authorities", trustedCertPath) .setSecureSettings(secureSettings) .build(); - try (HttpClient client = new HttpClient(settings, new SSLService(settings, environment), null, mockClusterService())) { + final SSLService ssl = new SSLService(TestEnvironment.newEnvironment(settings)); + try (HttpClient client = new HttpClient(settings, ssl, null, mockClusterService())) { secureSettings = new MockSecureSettings(); // We can't use the client created above for the server since it is only a truststore secureSettings.setString("xpack.security.http.ssl.secure_key_passphrase", "testnode"); Settings settings2 = Settings.builder() + .put(environment.settings()) .put("xpack.security.http.ssl.enabled", true) .put("xpack.security.http.ssl.key", keyPath) .put("xpack.security.http.ssl.certificate", certPath) @@ -204,7 +206,7 @@ public void testHttps() throws Exception { .setSecureSettings(secureSettings) .build(); - TestsSSLService sslService = new TestsSSLService(settings2, environment); + TestsSSLService sslService = new TestsSSLService(TestEnvironment.newEnvironment(settings2)); testSslMockWebserver(client, sslService.sslContext("xpack.security.http.ssl"), false); } } @@ -212,8 +214,8 @@ public void testHttps() throws Exception { public void testHttpsDisableHostnameVerification() throws Exception { Path certPath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode-no-subjaltname.crt"); Path keyPath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode-no-subjaltname.pem"); - Settings settings; Settings.Builder builder = Settings.builder() + .put(environment.settings()) .put("xpack.http.ssl.certificate_authorities", certPath); if (inFipsJvm()) { //Can't use TrustAllConfig in FIPS mode @@ -221,12 +223,14 @@ public void testHttpsDisableHostnameVerification() throws Exception { } else { builder.put("xpack.http.ssl.verification_mode", randomFrom(VerificationMode.NONE, VerificationMode.CERTIFICATE)); } - settings = builder.build(); - try (HttpClient client = new HttpClient(settings, new SSLService(settings, environment), null, mockClusterService())) { + final Settings settings = builder.build(); + final SSLService ssl = new SSLService(TestEnvironment.newEnvironment(settings)); + try (HttpClient client = new HttpClient(settings, ssl, null, mockClusterService())) { MockSecureSettings secureSettings = new MockSecureSettings(); // We can't use the client created above for the server since it only defines a truststore secureSettings.setString("xpack.security.http.ssl.secure_key_passphrase", "testnode-no-subjaltname"); Settings settings2 = Settings.builder() + .put(environment.settings()) .put("xpack.security.http.ssl.enabled", true) .put("xpack.security.http.ssl.key", keyPath) .put("xpack.security.http.ssl.certificate", certPath) @@ -234,8 +238,8 @@ public void testHttpsDisableHostnameVerification() throws Exception { .setSecureSettings(secureSettings) .build(); - TestsSSLService sslService = new TestsSSLService(settings2, environment); - testSslMockWebserver(client, sslService.sslContext("xpack.security.http.ssl"), false); + TestsSSLService ssl2 = new TestsSSLService(TestEnvironment.newEnvironment(settings2)); + testSslMockWebserver(client, ssl2.sslContext("xpack.security.http.ssl"), false); } } @@ -245,13 +249,14 @@ public void testHttpsClientAuth() throws Exception { MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("xpack.http.ssl.secure_key_passphrase", "testnode"); Settings settings = Settings.builder() + .put(environment.settings()) .put("xpack.http.ssl.key", keyPath) .put("xpack.http.ssl.certificate", certPath) .putList("xpack.http.ssl.supported_protocols", getProtocols()) .setSecureSettings(secureSettings) .build(); - TestsSSLService sslService = new TestsSSLService(settings, environment); + TestsSSLService sslService = new TestsSSLService(TestEnvironment.newEnvironment(settings)); try (HttpClient client = new HttpClient(settings, sslService, null, mockClusterService())) { testSslMockWebserver(client, sslService.sslContext("xpack.http.ssl"), true); } @@ -275,9 +280,9 @@ private void testSslMockWebserver(HttpClient client, SSLContext sslContext, bool } public void testHttpResponseWithAnyStatusCodeCanReturnBody() throws Exception { - int statusCode = randomFrom(200, 201, 400, 401, 403, 404, 405, 409, 413, 429, 500, 503); - String body = RandomStrings.randomAsciiOfLength(random(), 100); - boolean hasBody = usually(); + final int statusCode = randomFrom(200, 201, 400, 401, 403, 404, 405, 409, 413, 429, 500, 503); + final String body = randomAlphaOfLength(100); + final boolean hasBody = usually(); MockResponse mockResponse = new MockResponse().setResponseCode(statusCode); if (hasBody) { mockResponse.setBody(body); @@ -300,7 +305,7 @@ public void testHttpResponseWithAnyStatusCodeCanReturnBody() throws Exception { @Network public void testHttpsWithoutTruststore() throws Exception { - try (HttpClient client = new HttpClient(Settings.EMPTY, new SSLService(Settings.EMPTY, environment), null, mockClusterService())) { + try (HttpClient client = new HttpClient(Settings.EMPTY, new SSLService(environment), null, mockClusterService())) { // Known server with a valid cert from a commercial CA HttpRequest.Builder request = HttpRequest.builder("www.elastic.co", 443).scheme(Scheme.HTTPS); HttpResponse response = client.execute(request.build()); @@ -316,6 +321,7 @@ public void testThatProxyCanBeConfigured() throws Exception { proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody("fullProxiedContent")); proxyServer.start(); Settings settings = Settings.builder() + .put(environment.settings()) .put(HttpSettings.PROXY_HOST.getKey(), "localhost") .put(HttpSettings.PROXY_PORT.getKey(), proxyServer.getPort()) .build(); @@ -324,7 +330,8 @@ public void testThatProxyCanBeConfigured() throws Exception { .method(HttpMethod.GET) .path("/"); - try (HttpClient client = new HttpClient(settings, new SSLService(settings, environment), null, mockClusterService())) { + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(settings)); + try (HttpClient client = new HttpClient(settings, sslService, null, mockClusterService())) { HttpResponse response = client.execute(requestBuilder.build()); assertThat(response.status(), equalTo(200)); assertThat(response.body().utf8ToString(), equalTo("fullProxiedContent")); @@ -382,19 +389,21 @@ public void testProxyCanHaveDifferentSchemeThanRequest() throws Exception { // We can't use the client created above for the server since it is only a truststore serverSecureSettings.setString("xpack.http.ssl.secure_key_passphrase", "testnode"); Settings serverSettings = Settings.builder() + .put(environment.settings()) .put("xpack.http.ssl.key", keyPath) .put("xpack.http.ssl.certificate", certPath) .put("xpack.security.http.ssl.enabled", false) .putList("xpack.security.http.ssl.supported_protocols", getProtocols()) .setSecureSettings(serverSecureSettings) .build(); - TestsSSLService sslService = new TestsSSLService(serverSettings, environment); + TestsSSLService sslService = new TestsSSLService(TestEnvironment.newEnvironment(serverSettings)); try (MockWebServer proxyServer = new MockWebServer(sslService.sslContext(serverSettings.getByPrefix("xpack.http.ssl.")), false)) { proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody("fullProxiedContent")); proxyServer.start(); Settings settings = Settings.builder() + .put(environment.settings()) .put(HttpSettings.PROXY_HOST.getKey(), "localhost") .put(HttpSettings.PROXY_PORT.getKey(), proxyServer.getPort()) .put(HttpSettings.PROXY_SCHEME.getKey(), "https") @@ -408,7 +417,8 @@ public void testProxyCanHaveDifferentSchemeThanRequest() throws Exception { .scheme(Scheme.HTTP) .path("/"); - try (HttpClient client = new HttpClient(settings, new SSLService(settings, environment), null, mockClusterService())) { + final SSLService ssl = new SSLService(TestEnvironment.newEnvironment(settings)); + try (HttpClient client = new HttpClient(settings, ssl, null, mockClusterService())) { HttpResponse response = client.execute(requestBuilder.build()); assertThat(response.status(), equalTo(200)); assertThat(response.body().utf8ToString(), equalTo("fullProxiedContent")); @@ -426,6 +436,7 @@ public void testThatProxyCanBeOverriddenByRequest() throws Exception { proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody("fullProxiedContent")); proxyServer.start(); Settings settings = Settings.builder() + .put(environment.settings()) .put(HttpSettings.PROXY_HOST.getKey(), "localhost") .put(HttpSettings.PROXY_PORT.getKey(), proxyServer.getPort() + 1) .put(HttpSettings.PROXY_HOST.getKey(), "https") @@ -436,7 +447,8 @@ public void testThatProxyCanBeOverriddenByRequest() throws Exception { .proxy(new HttpProxy("localhost", proxyServer.getPort(), Scheme.HTTP)) .path("/"); - try (HttpClient client = new HttpClient(settings, new SSLService(settings, environment), null, mockClusterService())) { + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(settings)); + try (HttpClient client = new HttpClient(settings, sslService, null, mockClusterService())) { HttpResponse response = client.execute(requestBuilder.build()); assertThat(response.status(), equalTo(200)); assertThat(response.body().utf8ToString(), equalTo("fullProxiedContent")); @@ -449,15 +461,17 @@ public void testThatProxyCanBeOverriddenByRequest() throws Exception { } public void testThatProxyConfigurationRequiresHostAndPort() { - Settings.Builder settings = Settings.builder(); + Settings.Builder settings = Settings.builder().put(environment.settings()); if (randomBoolean()) { settings.put(HttpSettings.PROXY_HOST.getKey(), "localhost"); } else { settings.put(HttpSettings.PROXY_PORT.getKey(), 8080); } + final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(settings.build())); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, - () -> new HttpClient(settings.build(), new SSLService(settings.build(), environment), null, mockClusterService())); + () -> new HttpClient(settings.build(), sslService, null, + mockClusterService())); assertThat(e.getMessage(), containsString("HTTP proxy requires both settings: [xpack.http.proxy.host] and [xpack.http.proxy.port]")); } @@ -515,7 +529,7 @@ public void testThatClientTakesTimeoutsIntoAccountAfterHeadersAreSent() throws E public void testThatHttpClientFailsOnNonHttpResponse() throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor(); - AtomicReference hasExceptionHappened = new AtomicReference(); + AtomicReference hasExceptionHappened = new AtomicReference<>(); try (ServerSocket serverSocket = new MockServerSocket(0, 50, InetAddress.getByName("localhost"))) { executor.execute(() -> { try (Socket socket = serverSocket.accept()) { @@ -556,7 +570,7 @@ public void testMaxHttpResponseSize() throws Exception { HttpRequest.Builder requestBuilder = HttpRequest.builder("localhost", webServer.getPort()).method(HttpMethod.GET).path("/"); - try (HttpClient client = new HttpClient(settings, new SSLService(environment.settings(), environment), null, + try (HttpClient client = new HttpClient(settings, new SSLService(environment), null, mockClusterService())) { IOException e = expectThrows(IOException.class, () -> client.execute(requestBuilder.build())); assertThat(e.getMessage(), startsWith("Maximum limit of")); @@ -631,7 +645,7 @@ public void testThatWhiteListingWorks() throws Exception { webServer.enqueue(new MockResponse().setResponseCode(200).setBody("whatever")); Settings settings = Settings.builder().put(HttpSettings.HOSTS_WHITELIST.getKey(), getWebserverUri()).build(); - try (HttpClient client = new HttpClient(settings, new SSLService(environment.settings(), environment), null, + try (HttpClient client = new HttpClient(settings, new SSLService(environment), null, mockClusterService())) { HttpRequest request = HttpRequest.builder(webServer.getHostName(), webServer.getPort()).path("foo").build(); client.execute(request); @@ -643,7 +657,7 @@ public void testThatWhiteListBlocksRequests() throws Exception { .put(HttpSettings.HOSTS_WHITELIST.getKey(), getWebserverUri()) .build(); - try (HttpClient client = new HttpClient(settings, new SSLService(environment.settings(), environment), null, + try (HttpClient client = new HttpClient(settings, new SSLService(environment), null, mockClusterService())) { HttpRequest request = HttpRequest.builder("blocked.domain.org", webServer.getPort()) .path("foo") @@ -667,7 +681,7 @@ public void testThatWhiteListBlocksRedirects() throws Exception { Settings settings = Settings.builder().put(HttpSettings.HOSTS_WHITELIST.getKey(), getWebserverUri()).build(); - try (HttpClient client = new HttpClient(settings, new SSLService(environment.settings(), environment), null, + try (HttpClient client = new HttpClient(settings, new SSLService(environment), null, mockClusterService())) { HttpRequest request = HttpRequest.builder(webServer.getHostName(), webServer.getPort()).path("/") .method(method) @@ -688,7 +702,7 @@ public void testThatWhiteListingWorksForRedirects() throws Exception { Settings settings = Settings.builder().put(HttpSettings.HOSTS_WHITELIST.getKey(), getWebserverUri() + "*").build(); - try (HttpClient client = new HttpClient(settings, new SSLService(environment.settings(), environment), null, + try (HttpClient client = new HttpClient(settings, new SSLService(environment), null, mockClusterService())) { HttpRequest request = HttpRequest.builder(webServer.getHostName(), webServer.getPort()).path("/") .method(HttpMethod.GET) @@ -708,7 +722,7 @@ public void testThatWhiteListReloadingWorks() throws Exception { when(clusterService.getClusterSettings()).thenReturn(clusterSettings); try (HttpClient client = - new HttpClient(settings, new SSLService(environment.settings(), environment), null, clusterService)) { + new HttpClient(settings, new SSLService(environment), null, clusterService)) { // blacklisted HttpRequest request = HttpRequest.builder(webServer.getHostName(), webServer.getPort()).path("/") diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpConnectionTimeoutTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpConnectionTimeoutTests.java index 3451c771e3e60..94f5115d673fd 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpConnectionTimeoutTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpConnectionTimeoutTests.java @@ -25,7 +25,7 @@ public class HttpConnectionTimeoutTests extends ESTestCase { @Network public void testDefaultTimeout() throws Exception { Environment environment = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); - HttpClient httpClient = new HttpClient(Settings.EMPTY, new SSLService(environment.settings(), environment), null, + HttpClient httpClient = new HttpClient(Settings.EMPTY, new SSLService(environment), null, mockClusterService()); HttpRequest request = HttpRequest.builder(UNROUTABLE_IP, 12345) @@ -51,7 +51,7 @@ public void testDefaultTimeout() throws Exception { public void testDefaultTimeoutCustom() throws Exception { Environment environment = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); HttpClient httpClient = new HttpClient(Settings.builder() - .put("xpack.http.default_connection_timeout", "5s").build(), new SSLService(environment.settings(), environment), null, + .put("xpack.http.default_connection_timeout", "5s").build(), new SSLService(environment), null, mockClusterService()); HttpRequest request = HttpRequest.builder(UNROUTABLE_IP, 12345) @@ -77,7 +77,7 @@ public void testDefaultTimeoutCustom() throws Exception { public void testTimeoutCustomPerRequest() throws Exception { Environment environment = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); HttpClient httpClient = new HttpClient(Settings.builder() - .put("xpack.http.default_connection_timeout", "10s").build(), new SSLService(environment.settings(), environment), null, + .put("xpack.http.default_connection_timeout", "10s").build(), new SSLService(environment), null, mockClusterService()); HttpRequest request = HttpRequest.builder(UNROUTABLE_IP, 12345) diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpReadTimeoutTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpReadTimeoutTests.java index e534a2a90757e..918bc33d61cf4 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpReadTimeoutTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/HttpReadTimeoutTests.java @@ -44,7 +44,7 @@ public void testDefaultTimeout() throws Exception { .path("/") .build(); - try (HttpClient httpClient = new HttpClient(Settings.EMPTY, new SSLService(environment.settings(), environment), + try (HttpClient httpClient = new HttpClient(Settings.EMPTY, new SSLService(environment), null, mockClusterService())) { long start = System.nanoTime(); @@ -67,7 +67,7 @@ public void testDefaultTimeoutCustom() throws Exception { .build(); try (HttpClient httpClient = new HttpClient(Settings.builder() - .put("xpack.http.default_read_timeout", "3s").build(), new SSLService(environment.settings(), environment), + .put("xpack.http.default_read_timeout", "3s").build(), new SSLService(environment), null, mockClusterService())) { long start = System.nanoTime(); @@ -91,7 +91,7 @@ public void testTimeoutCustomPerRequest() throws Exception { .build(); try (HttpClient httpClient = new HttpClient(Settings.builder() - .put("xpack.http.default_read_timeout", "10s").build(), new SSLService(environment.settings(), environment), + .put("xpack.http.default_read_timeout", "10s").build(), new SSLService(environment), null, mockClusterService())) { long start = System.nanoTime(); diff --git a/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/test/OpenLdapTests.java b/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/test/OpenLdapTests.java index b763e3e985fb5..2e9ecdfbc67f4 100644 --- a/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/test/OpenLdapTests.java +++ b/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/test/OpenLdapTests.java @@ -102,7 +102,7 @@ public void initializeSslSocketFactory() throws Exception { builder.put("xpack.security.authc.realms.ldap.vmode_full.ssl.verification_mode", VerificationMode.FULL); globalSettings = builder.setSecureSettings(mockSecureSettings).build(); Environment environment = TestEnvironment.newEnvironment(globalSettings); - sslService = new SSLService(globalSettings, environment); + sslService = new SSLService(environment); } public void testConnect() throws Exception { diff --git a/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/xpack/security/authc/ldap/OpenLdapUserSearchSessionFactoryTests.java b/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/xpack/security/authc/ldap/OpenLdapUserSearchSessionFactoryTests.java index de1183db19391..74396e3ff9837 100644 --- a/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/xpack/security/authc/ldap/OpenLdapUserSearchSessionFactoryTests.java +++ b/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/xpack/security/authc/ldap/OpenLdapUserSearchSessionFactoryTests.java @@ -94,7 +94,7 @@ public void testUserSearchWithBindUserOpenLDAP() throws Exception { RealmConfig config = new RealmConfig(realmId, settings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); - SSLService sslService = new SSLService(settings, TestEnvironment.newEnvironment(settings)); + SSLService sslService = new SSLService(TestEnvironment.newEnvironment(settings)); String[] users = new String[]{"cap", "hawkeye", "hulk", "ironman", "thor"}; try (LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, sslService, threadPool)) { diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ADLdapUserSearchSessionFactoryTests.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ADLdapUserSearchSessionFactoryTests.java index d2c79d8882f46..54e6cd1e0ed0b 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ADLdapUserSearchSessionFactoryTests.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ADLdapUserSearchSessionFactoryTests.java @@ -37,7 +37,6 @@ public class ADLdapUserSearchSessionFactoryTests extends AbstractActiveDirectory @Before public void init() throws Exception { Path certPath = getDataPath("support/smb_ca.crt"); - Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()); /* * Prior to each test we reinitialize the socket factory with a new SSLService so that we get a new SSLContext. * If we re-use an SSLContext, previously connected sessions can get re-established which breaks hostname @@ -48,7 +47,8 @@ public void init() throws Exception { .put("path.home", createTempDir()) .put("xpack.security.authc.realms.ldap.ad-as-ldap-test.ssl.certificate_authorities", certPath) .build(); - sslService = new SSLService(globalSettings, env); + Environment env = TestEnvironment.newEnvironment(globalSettings); + sslService = new SSLService(env); threadPool = new TestThreadPool("ADLdapUserSearchSessionFactoryTests"); } @@ -77,7 +77,7 @@ public void testUserSearchWithActiveDirectory() throws Exception { }); Settings fullSettings = builder.build(); - sslService = new SSLService(fullSettings, TestEnvironment.newEnvironment(fullSettings)); + sslService = new SSLService(TestEnvironment.newEnvironment(fullSettings)); RealmConfig config = new RealmConfig(new RealmConfig.RealmIdentifier("ldap", "ad-as-ldap-test"), fullSettings, TestEnvironment.newEnvironment(fullSettings), new ThreadContext(fullSettings)); LdapUserSearchSessionFactory sessionFactory = getLdapUserSearchSessionFactory(config, sslService, threadPool); diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/AbstractActiveDirectoryTestCase.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/AbstractActiveDirectoryTestCase.java index df8b23d9381a1..e77c00b534b88 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/AbstractActiveDirectoryTestCase.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/AbstractActiveDirectoryTestCase.java @@ -90,7 +90,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO builder.put("xpack.security.authc.realms.active_directory.bar.ssl.verification_mode", VerificationMode.CERTIFICATE); globalSettings = builder.build(); Environment environment = TestEnvironment.newEnvironment(globalSettings); - sslService = new SSLService(globalSettings, environment); + sslService = new SSLService(environment); } Settings buildAdSettings(RealmConfig.RealmIdentifier realmId, String ldapUrl, String adDomainName, String userSearchDN, diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java index b122404507bc6..65936248b4db5 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java @@ -87,13 +87,13 @@ public void testAdAuth() throws Exception { } private RealmConfig configureRealm(String name, String type, Settings settings) { - final Environment env = TestEnvironment.newEnvironment(globalSettings); final Settings mergedSettings = Settings.builder() .put(settings) .normalizePrefix("xpack.security.authc.realms." + type + "." + name + ".") .put(globalSettings) .build(); - this.sslService = new SSLService(mergedSettings, env); + final Environment env = TestEnvironment.newEnvironment(mergedSettings); + this.sslService = new SSLService(env); final RealmConfig.RealmIdentifier identifier = new RealmConfig.RealmIdentifier(type, name); return new RealmConfig(identifier, mergedSettings, env, new ThreadContext(globalSettings)); } From 1fbf799882d2bd58b6a995d29f821b6c2d206825 Mon Sep 17 00:00:00 2001 From: j-bean Date: Mon, 6 Jan 2020 10:23:57 +0500 Subject: [PATCH 384/686] Security should not reload files that haven't changed (#50207) In security we currently monitor a set of files for changes: - config/role_mapping.yml (or alternative configured path) - config/roles.yml - config/users - config/users_roles This commit prevents unnecessary reloading when the file change actually doesn't change the internal structure. Closes: #50063 Co-authored-by: Anton Shuvaev --- .../org/elasticsearch/common/util/Maps.java | 19 ++++++++++ .../elasticsearch/common/util/MapsTests.java | 36 +++++++++++++++++++ .../authc/file/FileUserPasswdStore.java | 11 ++++-- .../authc/file/FileUserRolesStore.java | 11 ++++-- .../security/authc/support/DnRoleMapper.java | 10 ++++-- .../security/authz/store/FileRolesStore.java | 8 +++-- .../authc/file/FileUserPasswdStoreTests.java | 16 ++++++++- .../authc/file/FileUserRolesStoreTests.java | 13 ++++++- .../authc/support/DnRoleMapperTests.java | 12 +++++++ .../authz/store/FileRolesStoreTests.java | 15 +++++++- 10 files changed, 136 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/util/Maps.java b/server/src/main/java/org/elasticsearch/common/util/Maps.java index 175a2a24db562..6c3a367b15ee8 100644 --- a/server/src/main/java/org/elasticsearch/common/util/Maps.java +++ b/server/src/main/java/org/elasticsearch/common/util/Maps.java @@ -99,4 +99,23 @@ public static Map ofEntries(final Collection> entri return map; } + /** + * Returns {@code true} if the two specified maps are equal to one another. Two maps are considered equal if both represent identical + * mappings where values are checked with Objects.deepEquals. The primary use case is to check if two maps with array values are equal. + * + * @param left one map to be tested for equality + * @param right the other map to be tested for equality + * @return {@code true} if the two maps are equal + */ + public static boolean deepEquals(Map left, Map right) { + if (left == right) { + return true; + } + if (left == null || right == null || left.size() != right.size()) { + return false; + } + return left.entrySet().stream() + .allMatch(e -> right.containsKey(e.getKey()) && Objects.deepEquals(e.getValue(), right.get(e.getKey()))); + } + } diff --git a/server/src/test/java/org/elasticsearch/common/util/MapsTests.java b/server/src/test/java/org/elasticsearch/common/util/MapsTests.java index 6edffee268ecc..b670d6badab1c 100644 --- a/server/src/test/java/org/elasticsearch/common/util/MapsTests.java +++ b/server/src/test/java/org/elasticsearch/common/util/MapsTests.java @@ -22,13 +22,18 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import static java.util.Map.entry; +import static java.util.stream.Collectors.toMap; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; @@ -103,6 +108,31 @@ public void testOfEntries() { assertMapEntriesAndImmutability(map, entries); } + public void testDeepEquals() { + final Supplier keyGenerator = () -> randomAlphaOfLengthBetween(1, 5); + final Supplier arrayValueGenerator = () -> random().ints(randomInt(5)).toArray(); + final Map map = randomMap(randomInt(5), keyGenerator, arrayValueGenerator); + final Map mapCopy = map.entrySet().stream() + .collect(toMap(Map.Entry::getKey, e -> Arrays.copyOf(e.getValue(), e.getValue().length))); + + assertTrue(Maps.deepEquals(map, mapCopy)); + + final Map mapModified = mapCopy; + if (mapModified.isEmpty()) { + mapModified.put(keyGenerator.get(), arrayValueGenerator.get()); + } else { + if (randomBoolean()) { + final String randomKey = mapModified.keySet().toArray(new String[0])[randomInt(mapModified.size() - 1)]; + final int[] value = mapModified.get(randomKey); + mapModified.put(randomKey, randomValueOtherThanMany((v) -> Arrays.equals(v, value), arrayValueGenerator)); + } else { + mapModified.put(randomValueOtherThanMany(mapModified::containsKey, keyGenerator), arrayValueGenerator.get()); + } + } + + assertFalse(Maps.deepEquals(map, mapModified)); + } + private void assertMapEntries(final Map map, final Collection> entries) { for (var entry : entries) { assertThat("map [" + map + "] does not contain key [" + entry.getKey() + "]", map.keySet(), hasItem(entry.getKey())); @@ -160,4 +190,10 @@ private void assertMapEntriesAndImmutability( assertMapImmutability(map); } + private static Map randomMap(int size, Supplier keyGenerator, Supplier valueGenerator) { + final Map map = new HashMap<>(); + IntStream.range(0, size).forEach(i -> map.put(keyGenerator.get(), valueGenerator.get())); + return map; + } + } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java index bbb5d77b90f4a..dab7152c538df 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java @@ -5,14 +5,15 @@ */ package org.elasticsearch.xpack.security.authc.file; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.Maps; import org.elasticsearch.env.Environment; import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileWatcher; @@ -191,9 +192,13 @@ public void onFileDeleted(Path file) { @Override public void onFileChanged(Path file) { if (file.equals(FileUserPasswdStore.this.file)) { - logger.info("users file [{}] changed. updating users... )", file.toAbsolutePath()); + final Map previousUsers = users; users = parseFileLenient(file, logger, settings); - notifyRefresh(); + + if (Maps.deepEquals(previousUsers, users) == false) { + logger.info("users file [{}] changed. updating users... )", file.toAbsolutePath()); + notifyRefresh(); + } } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStore.java index e79621964e70e..1eb2e468c8ef2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStore.java @@ -5,13 +5,14 @@ */ package org.elasticsearch.xpack.security.authc.file; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.util.Maps; import org.elasticsearch.env.Environment; import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileWatcher; @@ -206,9 +207,13 @@ public void onFileDeleted(Path file) { @Override public void onFileChanged(Path file) { if (file.equals(FileUserRolesStore.this.file)) { - logger.info("users roles file [{}] changed. updating users roles...", file.toAbsolutePath()); + final Map previousUserRoles = userRoles; userRoles = parseFileLenient(file, logger); - notifyRefresh(); + + if (Maps.deepEquals(previousUserRoles, userRoles) == false) { + logger.info("users roles file [{}] changed. updating users roles...", file.toAbsolutePath()); + notifyRefresh(); + } } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapper.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapper.java index 1018c591617a9..f62c8521a69ff 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapper.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapper.java @@ -223,10 +223,14 @@ public void onFileDeleted(Path file) { @Override public void onFileChanged(Path file) { if (file.equals(DnRoleMapper.this.file)) { - logger.info("role mappings file [{}] changed for realm [{}/{}]. updating mappings...", file.toAbsolutePath(), - config.type(), config.name()); + final Map> previousDnRoles = dnRoles; dnRoles = parseFileLenient(file, logger, config.type(), config.name()); - notifyRefresh(); + + if (previousDnRoles.equals(dnRoles) == false) { + logger.info("role mappings file [{}] changed for realm [{}/{}]. updating mappings...", file.toAbsolutePath(), + config.type(), config.name()); + notifyRefresh(); + } } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java index b1059c46cc668..90b94d1275989 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java @@ -368,8 +368,6 @@ public synchronized void onFileChanged(Path file) { final Map previousPermissions = permissions; try { permissions = parseFile(file, logger, settings, licenseState, xContentRegistry); - logger.info("updated roles (roles file [{}] {})", file.toAbsolutePath(), - Files.exists(file) ? "changed" : "removed"); } catch (Exception e) { logger.error( (Supplier) () -> new ParameterizedMessage( @@ -383,7 +381,11 @@ public synchronized void onFileChanged(Path file) { .collect(Collectors.toSet()); final Set addedRoles = Sets.difference(permissions.keySet(), previousPermissions.keySet()); final Set changedRoles = Collections.unmodifiableSet(Sets.union(changedOrMissingRoles, addedRoles)); - listeners.forEach(c -> c.accept(changedRoles)); + if (changedRoles.isEmpty() == false) { + logger.info("updated roles (roles file [{}] {})", file.toAbsolutePath(), + Files.exists(file) ? "changed" : "removed"); + listeners.forEach(c -> c.accept(changedRoles)); + } } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java index e6f099e389741..42cd5530f306d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java @@ -53,7 +53,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { @Before public void init() { settings = Settings.builder() - .put("resource.reload.interval.high", "2s") + .put("resource.reload.interval.high", "100ms") .put("path.home", createTempDir()) .put("xpack.security.authc.password_hashing.algorithm", randomFrom("bcrypt", "bcrypt11", "pbkdf2", "pbkdf2_1000", "pbkdf2_50000")) @@ -103,6 +103,20 @@ public void testStore_AutoReload() throws Exception { watcherService.start(); + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + writer.append("\n"); + } + + watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH); + if (latch.getCount() != 1) { + fail("Listener should not be called as users passwords are not changed."); + } + + assertThat(store.userExists(username), is(true)); + result = store.verifyPassword(username, new SecureString("test123"), () -> user); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + assertThat(result.getUser(), is(user)); + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { writer.newLine(); writer.append("foobar:").append(new String(hasher.hash(new SecureString("barfoo")))); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStoreTests.java index 0fef08acefbd4..4e1dd0644911c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStoreTests.java @@ -54,7 +54,7 @@ public class FileUserRolesStoreTests extends ESTestCase { @Before public void init() { settings = Settings.builder() - .put("resource.reload.interval.high", "2s") + .put("resource.reload.interval.high", "100ms") .put("path.home", createTempDir()) .build(); env = TestEnvironment.newEnvironment(settings); @@ -102,6 +102,17 @@ public void testStoreAutoReload() throws Exception { watcherService.start(); + try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + writer.append("\n"); + } + + watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH); + if (latch.getCount() != 1) { + fail("Listener should not be called as users roles are not changed."); + } + + assertThat(store.roles("user1"), arrayContaining("role1", "role2", "role3")); + try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { writer.newLine(); writer.append("role4:user4\nrole5:user4\n"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java index 7f7899e65943c..2f62c35ba1130 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java @@ -112,6 +112,18 @@ public void testMapper_AutoReload() throws Exception { watcherService.start(); + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + writer.append("\n"); + } + + watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH); + if (latch.getCount() != 1) { + fail("Listener should not be called as roles mapping is not changed."); + } + + roles = mapper.resolveRoles("", Collections.singletonList("cn=shield,ou=marvel,o=superheros")); + assertThat(roles, contains("security")); + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { writer.newLine(); writer.append("fantastic_four:\n") diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java index 99ae113e15fe9..f0cc9840e5e13 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java @@ -332,7 +332,7 @@ public void testAutoReload() throws Exception { } Settings.Builder builder = Settings.builder() - .put("resource.reload.interval.high", "500ms") + .put("resource.reload.interval.high", "100ms") .put("path.home", home); Settings settings = builder.build(); Environment env = TestEnvironment.newEnvironment(settings); @@ -354,6 +354,19 @@ public void testAutoReload() throws Exception { watcherService.start(); + try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + writer.append("\n"); + } + + watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH); + if (latch.getCount() != 1) { + fail("Listener should not be called as roles are not changed."); + } + + descriptors = store.roleDescriptors(Collections.singleton("role1")); + assertThat(descriptors, notNullValue()); + assertEquals(1, descriptors.size()); + try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { writer.newLine(); writer.newLine(); From cf1d80b55678be7cc67cb00be8eb7cd5bd042e54 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Mon, 6 Jan 2020 10:47:52 +0100 Subject: [PATCH 385/686] Re-enable FullClusterRestartIT#testWatcher test (#50463) Previously this test failed waiting for yellow: https://gradle-enterprise.elastic.co/s/fv55holsa36tg/console-log#L2676 Oddly cluster health returned red status, but there were no unassigned, relocating or initializing shards. Placed the waiting for green in a try-catch block, so that when this fails again then cluster state gets printed. Relates to #48381 --- .../xpack/restart/FullClusterRestartIT.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java b/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java index b14e3bb86ce14..e8c1b6e94ccf6 100644 --- a/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java +++ b/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java @@ -123,7 +123,6 @@ public void testSecurityNativeRealm() throws Exception { } @SuppressWarnings("unchecked") - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/48381") public void testWatcher() throws Exception { if (isRunningAgainstOldCluster()) { logger.info("Adding a watch on old cluster {}", getOldClusterVersion()); @@ -143,13 +142,25 @@ public void testWatcher() throws Exception { client().performRequest(createFunnyTimeout); logger.info("Waiting for watch results index to fill up..."); - waitForYellow(".watches,bwc_watch_index,.watcher-history*"); + try { + waitForYellow(".watches,bwc_watch_index,.watcher-history*"); + } catch (ResponseException e) { + String rsp = toStr(client().performRequest(new Request("GET", "/_cluster/state"))); + logger.info("cluster_state_response=\n{}", rsp); + throw e; + } waitForHits("bwc_watch_index", 2); waitForHits(".watcher-history*", 2); logger.info("Done creating watcher-related indices"); } else { logger.info("testing against {}", getOldClusterVersion()); - waitForYellow(".watches,bwc_watch_index,.watcher-history*"); + try { + waitForYellow(".watches,bwc_watch_index,.watcher-history*"); + } catch (ResponseException e) { + String rsp = toStr(client().performRequest(new Request("GET", "/_cluster/state"))); + logger.info("cluster_state_response=\n{}", rsp); + throw e; + } logger.info("checking that the Watches index is the correct version"); From 52cc1384fc6a21e6390d545f8a4c49740e2f9e17 Mon Sep 17 00:00:00 2001 From: j-bean Date: Mon, 6 Jan 2020 15:03:15 +0500 Subject: [PATCH 386/686] Add 'monitor_snapshot' cluster privilege (#50489) This adds a new cluster privilege `monitor_snapshot` which is a restricted version of `create_snapshot`, granting the same privileges to view snapshot and repository info and status but not granting the actual privilege to create a snapshot. Co-authored-by: Anton Shuvaev --- .../security/get-builtin-privileges.asciidoc | 1 + .../authorization/privileges.asciidoc | 3 ++ .../privilege/ClusterPrivilegeResolver.java | 4 +++ .../integration/ClusterPrivilegeTests.java | 33 +++++++++++++++++-- .../test/privileges/11_builtin.yml | 2 +- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc b/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc index cedffe7a6f9d4..e5af329b63843 100644 --- a/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc +++ b/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc @@ -87,6 +87,7 @@ A successful call returns an object with "cluster" and "index" fields. "monitor_data_frame_transforms", "monitor_ml", "monitor_rollup", + "monitor_snapshot", "monitor_transform", "monitor_watcher", "none", diff --git a/x-pack/docs/en/security/authorization/privileges.asciidoc b/x-pack/docs/en/security/authorization/privileges.asciidoc index aed7b236e172d..1d53c7b20aa6a 100644 --- a/x-pack/docs/en/security/authorization/privileges.asciidoc +++ b/x-pack/docs/en/security/authorization/privileges.asciidoc @@ -16,6 +16,9 @@ settings update, rerouting, or managing users and roles. Privileges to create snapshots for existing repositories. Can also list and view details on existing repositories and snapshots. +`monitor_snapshot`:: +Privileges to list and view details on existing repositories and snapshots. + `manage`:: Builds on `monitor` and adds cluster operations that change values in the cluster. This includes snapshotting, updating settings, and rerouting. It also includes diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java index 65469f77a116c..c9754e586c0a6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java @@ -66,6 +66,8 @@ public class ClusterPrivilegeResolver { Set.of("cluster:admin/xpack/ccr/*", ClusterStateAction.NAME, HasPrivilegesAction.NAME); private static final Set CREATE_SNAPSHOT_PATTERN = Set.of(CreateSnapshotAction.NAME, SnapshotsStatusAction.NAME + "*", GetSnapshotsAction.NAME, SnapshotsStatusAction.NAME, GetRepositoriesAction.NAME); + private static final Set MONITOR_SNAPSHOT_PATTERN = Set.of(SnapshotsStatusAction.NAME + "*", GetSnapshotsAction.NAME, + SnapshotsStatusAction.NAME, GetRepositoriesAction.NAME); private static final Set READ_CCR_PATTERN = Set.of(ClusterStateAction.NAME, HasPrivilegesAction.NAME); private static final Set MANAGE_ILM_PATTERN = Set.of("cluster:admin/ilm/*"); private static final Set READ_ILM_PATTERN = Set.of(GetLifecycleAction.NAME, GetStatusAction.NAME); @@ -109,6 +111,7 @@ public class ClusterPrivilegeResolver { public static final NamedClusterPrivilege MANAGE_CCR = new ActionClusterPrivilege("manage_ccr", MANAGE_CCR_PATTERN); public static final NamedClusterPrivilege READ_CCR = new ActionClusterPrivilege("read_ccr", READ_CCR_PATTERN); public static final NamedClusterPrivilege CREATE_SNAPSHOT = new ActionClusterPrivilege("create_snapshot", CREATE_SNAPSHOT_PATTERN); + public static final NamedClusterPrivilege MONITOR_SNAPSHOT = new ActionClusterPrivilege("monitor_snapshot", MONITOR_SNAPSHOT_PATTERN); public static final NamedClusterPrivilege MANAGE_ILM = new ActionClusterPrivilege("manage_ilm", MANAGE_ILM_PATTERN); public static final NamedClusterPrivilege READ_ILM = new ActionClusterPrivilege("read_ilm", READ_ILM_PATTERN); public static final NamedClusterPrivilege MANAGE_SLM = new ActionClusterPrivilege("manage_slm", MANAGE_SLM_PATTERN); @@ -146,6 +149,7 @@ public class ClusterPrivilegeResolver { MANAGE_CCR, READ_CCR, CREATE_SNAPSHOT, + MONITOR_SNAPSHOT, MANAGE_ILM, READ_ILM, MANAGE_SLM, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClusterPrivilegeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClusterPrivilegeTests.java index c434d3b182888..c699e21aed72b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClusterPrivilegeTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClusterPrivilegeTests.java @@ -36,13 +36,17 @@ public class ClusterPrivilegeTests extends AbstractPrivilegeTestCase { " - names: 'someindex'\n" + " privileges: [ all ]\n" + "role_d:\n" + - " cluster: [ create_snapshot ]\n"; + " cluster: [ create_snapshot ]\n" + + "\n" + + "role_e:\n" + + " cluster: [ monitor_snapshot]\n"; private static final String USERS_ROLES = "role_a:user_a\n" + "role_b:user_b\n" + "role_c:user_c\n" + - "role_d:user_d\n"; + "role_d:user_d\n" + + "role_e:user_e\n"; private static Path repositoryLocation; @@ -81,7 +85,8 @@ protected String configUsers() { "user_a:" + usersPasswdHashed + "\n" + "user_b:" + usersPasswdHashed + "\n" + "user_c:" + usersPasswdHashed + "\n" + - "user_d:" + usersPasswdHashed + "\n"; + "user_d:" + usersPasswdHashed + "\n" + + "user_e:" + usersPasswdHashed + "\n"; } @Override @@ -139,6 +144,19 @@ public void testThatClusterPrivilegesWorkAsExpectedViaHttp() throws Exception { assertAccessIsDenied("user_d", "GET", "/_nodes/infos"); assertAccessIsDenied("user_d", "POST", "/_cluster/reroute"); assertAccessIsDenied("user_d", "PUT", "/_cluster/settings", "{ \"transient\" : { \"search.default_search_timeout\": \"1m\" } }"); + + // user_e can view repos and snapshots on existing repos, everything else is DENIED + assertAccessIsDenied("user_e", "GET", "/_cluster/state"); + assertAccessIsDenied("user_e", "GET", "/_cluster/health"); + assertAccessIsDenied("user_e", "GET", "/_cluster/settings"); + assertAccessIsDenied("user_e", "GET", "/_cluster/stats"); + assertAccessIsDenied("user_e", "GET", "/_cluster/pending_tasks"); + assertAccessIsDenied("user_e", "GET", "/_nodes/stats"); + assertAccessIsDenied("user_e", "GET", "/_nodes/hot_threads"); + assertAccessIsDenied("user_e", "GET", "/_nodes/infos"); + assertAccessIsDenied("user_e", "POST", "/_cluster/reroute"); + assertAccessIsDenied("user_e", "PUT", "/_cluster/settings", "{ \"transient\" : { \"search.default_search_timeout\": \"1m\" } }"); + } public void testThatSnapshotAndRestore() throws Exception { @@ -147,6 +165,7 @@ public void testThatSnapshotAndRestore() throws Exception { assertAccessIsDenied("user_b", "PUT", "/_snapshot/my-repo", repoJson); assertAccessIsDenied("user_c", "PUT", "/_snapshot/my-repo", repoJson); assertAccessIsDenied("user_d", "PUT", "/_snapshot/my-repo", repoJson); + assertAccessIsDenied("user_e", "PUT", "/_snapshot/my-repo", repoJson); assertAccessIsAllowed("user_a", "PUT", "/_snapshot/my-repo", repoJson); Request createBar = new Request("PUT", "/someindex/_doc/1"); @@ -155,16 +174,19 @@ public void testThatSnapshotAndRestore() throws Exception { assertAccessIsDenied("user_a", createBar); assertAccessIsDenied("user_b", createBar); assertAccessIsDenied("user_d", createBar); + assertAccessIsDenied("user_e", createBar); assertAccessIsAllowed("user_c", createBar); assertAccessIsDenied("user_b", "PUT", "/_snapshot/my-repo/my-snapshot", "{ \"indices\": \"someindex\" }"); assertAccessIsDenied("user_c", "PUT", "/_snapshot/my-repo/my-snapshot", "{ \"indices\": \"someindex\" }"); + assertAccessIsDenied("user_e", "PUT", "/_snapshot/my-repo/my-snapshot", "{ \"indices\": \"someindex\" }"); assertAccessIsAllowed("user_a", "PUT", "/_snapshot/my-repo/my-snapshot", "{ \"indices\": \"someindex\" }"); assertAccessIsDenied("user_b", "GET", "/_snapshot/my-repo/my-snapshot/_status"); assertAccessIsDenied("user_c", "GET", "/_snapshot/my-repo/my-snapshot/_status"); assertAccessIsAllowed("user_a", "GET", "/_snapshot/my-repo/my-snapshot/_status"); assertAccessIsAllowed("user_d", "GET", "/_snapshot/my-repo/my-snapshot/_status"); + assertAccessIsAllowed("user_e", "GET", "/_snapshot/my-repo/my-snapshot/_status"); // This snapshot needs to be finished in order to be restored waitForSnapshotToFinish("my-repo", "my-snapshot"); @@ -175,6 +197,7 @@ public void testThatSnapshotAndRestore() throws Exception { assertAccessIsDenied("user_a", "DELETE", "/someindex"); assertAccessIsDenied("user_b", "DELETE", "/someindex"); assertAccessIsDenied("user_d", "DELETE", "/someindex"); + assertAccessIsDenied("user_e", "DELETE", "/someindex"); assertAccessIsAllowed("user_c", "DELETE", "/someindex"); Request restoreSnapshotRequest = new Request("POST", "/_snapshot/my-repo/my-snapshot/_restore"); @@ -182,21 +205,25 @@ public void testThatSnapshotAndRestore() throws Exception { assertAccessIsDenied("user_b", restoreSnapshotRequest); assertAccessIsDenied("user_c", restoreSnapshotRequest); assertAccessIsDenied("user_d", restoreSnapshotRequest); + assertAccessIsDenied("user_e", restoreSnapshotRequest); assertAccessIsAllowed("user_a", restoreSnapshotRequest); assertAccessIsDenied("user_a", "GET", "/someindex/_doc/1"); assertAccessIsDenied("user_b", "GET", "/someindex/_doc/1"); assertAccessIsDenied("user_d", "GET", "/someindex/_doc/1"); + assertAccessIsDenied("user_e", "GET", "/someindex/_doc/1"); assertAccessIsAllowed("user_c", "GET", "/someindex/_doc/1"); assertAccessIsDenied("user_b", "DELETE", "/_snapshot/my-repo/my-snapshot"); assertAccessIsDenied("user_c", "DELETE", "/_snapshot/my-repo/my-snapshot"); assertAccessIsDenied("user_d", "DELETE", "/_snapshot/my-repo/my-snapshot"); + assertAccessIsDenied("user_e", "DELETE", "/_snapshot/my-repo/my-snapshot"); assertAccessIsAllowed("user_a", "DELETE", "/_snapshot/my-repo/my-snapshot"); assertAccessIsDenied("user_b", "DELETE", "/_snapshot/my-repo"); assertAccessIsDenied("user_c", "DELETE", "/_snapshot/my-repo"); assertAccessIsDenied("user_d", "DELETE", "/_snapshot/my-repo"); + assertAccessIsDenied("user_e", "DELETE", "/_snapshot/my-repo"); assertAccessIsAllowed("user_a", "DELETE", "/_snapshot/my-repo"); } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml index c7130faf27749..02961a2db12f8 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml @@ -15,5 +15,5 @@ setup: # This is fragile - it needs to be updated every time we add a new cluster/index privilege # I would much prefer we could just check that specific entries are in the array, but we don't have # an assertion for that - - length: { "cluster" : 33 } + - length: { "cluster" : 34 } - length: { "index" : 17 } From 3e449ac8e6acb201561ea85a89de55380e539421 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 6 Jan 2020 11:07:35 +0000 Subject: [PATCH 387/686] Collect shard sizes for closed indices (#50645) Today the `InternalClusterInfoService` collects information on the sizes of shards of open indices, but does not consider closed indices. This means that shards of closed indices are treated as having zero size when they are being allocated. This commit fixes this, obtaining the sizes of all shards. Relates #33888 --- .../elasticsearch/cluster/InternalClusterInfoService.java | 2 ++ .../java/org/elasticsearch/cluster/ClusterInfoServiceIT.java | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/InternalClusterInfoService.java b/server/src/main/java/org/elasticsearch/cluster/InternalClusterInfoService.java index d07897199ae8e..8c49c3de39690 100644 --- a/server/src/main/java/org/elasticsearch/cluster/InternalClusterInfoService.java +++ b/server/src/main/java/org/elasticsearch/cluster/InternalClusterInfoService.java @@ -30,6 +30,7 @@ import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest; import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; import org.elasticsearch.action.admin.indices.stats.ShardStats; +import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -258,6 +259,7 @@ protected CountDownLatch updateIndicesStats(final ActionListener(listener, latch)); return latch; diff --git a/server/src/test/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java b/server/src/test/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java index aa897f10bb895..6db2097f7d359 100644 --- a/server/src/test/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java +++ b/server/src/test/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java @@ -114,11 +114,14 @@ private void setClusterInfoTimeout(String timeValue) { .put(InternalClusterInfoService.INTERNAL_CLUSTER_INFO_TIMEOUT_SETTING.getKey(), timeValue).build())); } - public void testClusterInfoServiceCollectsInformation() throws Exception { + public void testClusterInfoServiceCollectsInformation() { internalCluster().startNodes(2); assertAcked(prepareCreate("test").setSettings(Settings.builder() .put(Store.INDEX_STORE_STATS_REFRESH_INTERVAL_SETTING.getKey(), 0) .put(EnableAllocationDecider.INDEX_ROUTING_REBALANCE_ENABLE_SETTING.getKey(), EnableAllocationDecider.Rebalance.NONE).build())); + if (randomBoolean()) { + assertAcked(client().admin().indices().prepareClose("test")); + } ensureGreen("test"); InternalTestCluster internalTestCluster = internalCluster(); // Get the cluster info service on the master node From cb1edde31119a9010636bae52321080ae6820bbe Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Mon, 6 Jan 2020 12:18:12 +0100 Subject: [PATCH 388/686] Workaround for JDK 14 EA FileChannel.map issue (#50523) FileChannel.map provokes static initialization of ExtendedMapMode in JDK14 EA, which needs elevated privileges. Relates #50512 --- .../elasticsearch/bootstrap/Bootstrap.java | 24 +++++++++++++++++-- .../bootstrap/BootstrapForTesting.java | 2 ++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java b/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java index f59f082596d7b..2c3184ed9812b 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java @@ -19,14 +19,13 @@ package org.elasticsearch.bootstrap; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.appender.ConsoleAppender; import org.apache.logging.log4j.core.config.Configurator; import org.apache.lucene.util.Constants; -import org.elasticsearch.core.internal.io.IOUtils; import org.apache.lucene.util.StringHelper; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; @@ -41,6 +40,7 @@ import org.elasticsearch.common.settings.SecureSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.monitor.jvm.JvmInfo; import org.elasticsearch.monitor.os.OsProbe; @@ -158,6 +158,24 @@ static void initializeProbes() { JvmInfo.jvmInfo(); } + /** + * JDK 14 bug: + * https://github.com/elastic/elasticsearch/issues/50512 + * We circumvent it here by loading the offending class before installing security manager. + * + * To be removed once the JDK is fixed. + */ + static void fixJDK14EAFileChannelMap() { + // minor time-bomb here to ensure that we reevaluate if final 14 version does not include fix. + if (System.getProperty("java.version").equals("14-ea")) { + try { + Class.forName("jdk.internal.misc.ExtendedMapMode", true, Bootstrap.class.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to lookup ExtendedMapMode class", e); + } + } + } + private void setup(boolean addShutdownHook, Environment environment) throws BootstrapException { Settings settings = environment.settings(); @@ -209,6 +227,8 @@ public void run() { // Log ifconfig output before SecurityManager is installed IfConfig.logIfNecessary(); + fixJDK14EAFileChannelMap(); + // install SM after natives, shutdown hooks, etc. try { Security.configure(environment, BootstrapSettings.SECURITY_FILTER_BAD_DEFAULTS_SETTING.get(settings)); diff --git a/test/framework/src/main/java/org/elasticsearch/bootstrap/BootstrapForTesting.java b/test/framework/src/main/java/org/elasticsearch/bootstrap/BootstrapForTesting.java index e035b779b3f02..c67eea7524951 100644 --- a/test/framework/src/main/java/org/elasticsearch/bootstrap/BootstrapForTesting.java +++ b/test/framework/src/main/java/org/elasticsearch/bootstrap/BootstrapForTesting.java @@ -102,6 +102,8 @@ public class BootstrapForTesting { // Log ifconfig output before SecurityManager is installed IfConfig.logIfNecessary(); + Bootstrap.fixJDK14EAFileChannelMap(); + // install security manager if requested if (systemPropertyAsBoolean("tests.security.manager", true)) { try { From ed50a37e9967220d2345ae6ecf34196e720f8ce4 Mon Sep 17 00:00:00 2001 From: Nikita Glashenko Date: Mon, 6 Jan 2020 15:46:08 +0400 Subject: [PATCH 389/686] Add tests for remaining IntervalsSourceProvider implementations (#50326) This PR adds unit tests for wire and xContent serialization of remaining IntervalsSourceProvider implementations. Closes #50150 --- .../index/query/IntervalsSourceProvider.java | 95 +++++++++++++++- .../elasticsearch/search/SearchModule.java | 30 +++--- .../CombineIntervalsSourceProviderTests.java | 88 +++++++++++++++ ...sjunctionIntervalsSourceProviderTests.java | 75 +++++++++++++ .../FilterIntervalsSourceProviderTests.java | 87 +++++++++++++++ .../FuzzyIntervalsSourceProviderTests.java | 101 ++++++++++++++++++ .../query/IntervalQueryBuilderTests.java | 60 +++++++---- .../MatchIntervalsSourceProviderTests.java | 93 ++++++++++++++++ .../WildcardIntervalsSourceProviderTests.java | 4 + 9 files changed, 594 insertions(+), 39 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/index/query/CombineIntervalsSourceProviderTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/query/DisjunctionIntervalsSourceProviderTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/query/FilterIntervalsSourceProviderTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/query/FuzzyIntervalsSourceProviderTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/query/MatchIntervalsSourceProviderTests.java diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java index dbd8f339ca66f..1f015af9b7f98 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java @@ -246,6 +246,30 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public static Match fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } + + String getQuery() { + return query; + } + + int getMaxGaps() { + return maxGaps; + } + + boolean isOrdered() { + return ordered; + } + + String getAnalyzer() { + return analyzer; + } + + IntervalFilter getFilter() { + return filter; + } + + String getUseField() { + return useField; + } } public static class Disjunction extends IntervalsSourceProvider { @@ -290,12 +314,13 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Disjunction that = (Disjunction) o; - return Objects.equals(subSources, that.subSources); + return Objects.equals(subSources, that.subSources) && + Objects.equals(filter, that.filter); } @Override public int hashCode() { - return Objects.hash(subSources); + return Objects.hash(subSources, filter); } @Override @@ -342,6 +367,14 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public static Disjunction fromXContent(XContentParser parser) throws IOException { return PARSER.parse(parser, null); } + + List getSubSources() { + return subSources; + } + + IntervalFilter getFilter() { + return filter; + } } public static class Combine extends IntervalsSourceProvider { @@ -393,12 +426,14 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Combine combine = (Combine) o; return Objects.equals(subSources, combine.subSources) && - ordered == combine.ordered && maxGaps == combine.maxGaps; + ordered == combine.ordered && + maxGaps == combine.maxGaps && + Objects.equals(filter, combine.filter); } @Override public int hashCode() { - return Objects.hash(subSources, ordered, maxGaps); + return Objects.hash(subSources, ordered, maxGaps, filter); } @Override @@ -452,6 +487,22 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public static Combine fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } + + List getSubSources() { + return subSources; + } + + boolean isOrdered() { + return ordered; + } + + int getMaxGaps() { + return maxGaps; + } + + IntervalFilter getFilter() { + return filter; + } } public static class Prefix extends IntervalsSourceProvider { @@ -838,6 +889,30 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public static Fuzzy fromXContent(XContentParser parser) throws IOException { return PARSER.parse(parser, null); } + + String getTerm() { + return term; + } + + int getPrefixLength() { + return prefixLength; + } + + boolean isTranspositions() { + return transpositions; + } + + Fuzziness getFuzziness() { + return fuzziness; + } + + String getAnalyzer() { + return analyzer; + } + + String getUseField() { + return useField; + } } static class ScriptFilterSource extends FilteredIntervalsSource { @@ -985,6 +1060,18 @@ public static IntervalFilter fromXContent(XContentParser parser) throws IOExcept } return new IntervalFilter(intervals, type); } + + String getType() { + return type; + } + + IntervalsSourceProvider getFilter() { + return filter; + } + + Script getScript() { + return script; + } } diff --git a/server/src/main/java/org/elasticsearch/search/SearchModule.java b/server/src/main/java/org/elasticsearch/search/SearchModule.java index ca64b749a5809..0be9317242c68 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/server/src/main/java/org/elasticsearch/search/SearchModule.java @@ -794,18 +794,24 @@ private void registerQueryParsers(List plugins) { } private void registerIntervalsSourceProviders() { - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Match.NAME, IntervalsSourceProvider.Match::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Combine.NAME, IntervalsSourceProvider.Combine::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Disjunction.NAME, IntervalsSourceProvider.Disjunction::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Prefix.NAME, IntervalsSourceProvider.Prefix::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Wildcard.NAME, IntervalsSourceProvider.Wildcard::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Fuzzy.NAME, IntervalsSourceProvider.Fuzzy::new)); + namedWriteables.addAll(getIntervalsSourceProviderNamedWritables()); + } + + public static List getIntervalsSourceProviderNamedWritables() { + return List.of( + new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, + IntervalsSourceProvider.Match.NAME, IntervalsSourceProvider.Match::new), + new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, + IntervalsSourceProvider.Combine.NAME, IntervalsSourceProvider.Combine::new), + new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, + IntervalsSourceProvider.Disjunction.NAME, IntervalsSourceProvider.Disjunction::new), + new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, + IntervalsSourceProvider.Prefix.NAME, IntervalsSourceProvider.Prefix::new), + new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, + IntervalsSourceProvider.Wildcard.NAME, IntervalsSourceProvider.Wildcard::new), + new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, + IntervalsSourceProvider.Fuzzy.NAME, IntervalsSourceProvider.Fuzzy::new) + ); } private void registerQuery(QuerySpec spec) { diff --git a/server/src/test/java/org/elasticsearch/index/query/CombineIntervalsSourceProviderTests.java b/server/src/test/java/org/elasticsearch/index/query/CombineIntervalsSourceProviderTests.java new file mode 100644 index 0000000000000..f65a08d2b2b12 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/CombineIntervalsSourceProviderTests.java @@ -0,0 +1,88 @@ +/* + * 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. + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.index.query.IntervalsSourceProvider.Combine; + +public class CombineIntervalsSourceProviderTests extends AbstractSerializingTestCase { + + @Override + protected Combine createTestInstance() { + return IntervalQueryBuilderTests.createRandomCombine(0, randomBoolean()); + } + + @Override + protected Combine mutateInstance(Combine instance) throws IOException { + List subSources = instance.getSubSources(); + boolean ordered = instance.isOrdered(); + int maxGaps = instance.getMaxGaps(); + IntervalsSourceProvider.IntervalFilter filter = instance.getFilter(); + switch (between(0, 3)) { + case 0: + subSources = subSources == null ? + IntervalQueryBuilderTests.createRandomSourceList(0, randomBoolean(), randomInt(5) + 1) : + null; + break; + case 1: + ordered = !ordered; + break; + case 2: + maxGaps++; + break; + case 3: + filter = filter == null ? + IntervalQueryBuilderTests.createRandomNonNullFilter(0, randomBoolean()) : + FilterIntervalsSourceProviderTests.mutateFilter(filter); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + return new Combine(subSources, ordered, maxGaps, filter); + } + + @Override + protected Writeable.Reader instanceReader() { + return Combine::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(SearchModule.getIntervalsSourceProviderNamedWritables()); + } + + @Override + protected Combine doParseInstance(XContentParser parser) throws IOException { + if (parser.nextToken() == XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + Combine combine = (Combine) IntervalsSourceProvider.fromXContent(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + return combine; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/query/DisjunctionIntervalsSourceProviderTests.java b/server/src/test/java/org/elasticsearch/index/query/DisjunctionIntervalsSourceProviderTests.java new file mode 100644 index 0000000000000..0daf48df07ba6 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/DisjunctionIntervalsSourceProviderTests.java @@ -0,0 +1,75 @@ +/* + * 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. + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.index.query.IntervalsSourceProvider.Disjunction; + +public class DisjunctionIntervalsSourceProviderTests extends AbstractSerializingTestCase { + + @Override + protected Disjunction createTestInstance() { + return IntervalQueryBuilderTests.createRandomDisjunction(0, randomBoolean()); + } + + @Override + protected Disjunction mutateInstance(Disjunction instance) throws IOException { + List subSources = instance.getSubSources(); + IntervalsSourceProvider.IntervalFilter filter = instance.getFilter(); + if (randomBoolean()) { + subSources = subSources == null ? + IntervalQueryBuilderTests.createRandomSourceList(0, randomBoolean(), randomInt(5) + 1) : + null; + } else { + filter = filter == null ? + IntervalQueryBuilderTests.createRandomNonNullFilter(0, randomBoolean()) : + FilterIntervalsSourceProviderTests.mutateFilter(filter); + } + return new Disjunction(subSources, filter); + } + + @Override + protected Writeable.Reader instanceReader() { + return Disjunction::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(SearchModule.getIntervalsSourceProviderNamedWritables()); + } + + @Override + protected Disjunction doParseInstance(XContentParser parser) throws IOException { + if (parser.nextToken() == XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + Disjunction disjunction = (Disjunction) IntervalsSourceProvider.fromXContent(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + return disjunction; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/query/FilterIntervalsSourceProviderTests.java b/server/src/test/java/org/elasticsearch/index/query/FilterIntervalsSourceProviderTests.java new file mode 100644 index 0000000000000..9e7c5e84a0c80 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/FilterIntervalsSourceProviderTests.java @@ -0,0 +1,87 @@ +/* + * 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. + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; +import java.util.Collections; + +import static org.elasticsearch.index.query.IntervalsSourceProvider.IntervalFilter; + +public class FilterIntervalsSourceProviderTests extends AbstractSerializingTestCase { + + @Override + protected IntervalFilter createTestInstance() { + return IntervalQueryBuilderTests.createRandomNonNullFilter(0, randomBoolean()); + } + + @Override + protected IntervalFilter mutateInstance(IntervalFilter instance) throws IOException { + return mutateFilter(instance); + } + + static IntervalFilter mutateFilter(IntervalFilter instance) { + IntervalsSourceProvider filter = instance.getFilter(); + String type = instance.getType(); + Script script = instance.getScript(); + + if (filter != null) { + if (randomBoolean()) { + if (filter instanceof IntervalsSourceProvider.Match) { + filter = WildcardIntervalsSourceProviderTests.createRandomWildcard(); + } else { + filter = IntervalQueryBuilderTests.createRandomMatch(0, randomBoolean()); + } + } else { + if (type.equals("containing")) { + type = "overlapping"; + } else { + type = "containing"; + } + } + return new IntervalFilter(filter, type); + } else { + return new IntervalFilter(new Script(ScriptType.INLINE, "mockscript", script.getIdOrCode() + "foo", Collections.emptyMap())); + } + } + + @Override + protected Writeable.Reader instanceReader() { + return IntervalFilter::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(SearchModule.getIntervalsSourceProviderNamedWritables()); + } + + @Override + protected IntervalFilter doParseInstance(XContentParser parser) throws IOException { + parser.nextToken(); + return IntervalFilter.fromXContent(parser); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/query/FuzzyIntervalsSourceProviderTests.java b/server/src/test/java/org/elasticsearch/index/query/FuzzyIntervalsSourceProviderTests.java new file mode 100644 index 0000000000000..ade0777740868 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/FuzzyIntervalsSourceProviderTests.java @@ -0,0 +1,101 @@ +/* + * 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. + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.unit.Fuzziness; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.IntervalsSourceProvider.Fuzzy; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; + +public class FuzzyIntervalsSourceProviderTests extends AbstractSerializingTestCase { + @Override + protected Fuzzy createTestInstance() { + return new Fuzzy( + randomAlphaOfLength(10), + randomInt(5), + randomBoolean(), + Fuzziness.fromEdits(randomInt(2)), + randomBoolean() ? null : randomAlphaOfLength(10), + randomBoolean() ? null : randomAlphaOfLength(10) + ); + } + + @Override + protected Fuzzy mutateInstance(Fuzzy instance) throws IOException { + String term = instance.getTerm(); + int prefixLength = instance.getPrefixLength(); + boolean isTranspositions = instance.isTranspositions(); + Fuzziness fuzziness = instance.getFuzziness(); + String analyzer = instance.getAnalyzer(); + String useField = instance.getUseField(); + switch (between(0, 5)) { + case 0: + term = randomAlphaOfLength(5); + break; + case 1: + prefixLength++; + break; + case 2: + isTranspositions = !isTranspositions; + break; + case 3: + if (fuzziness.equals(Fuzziness.ZERO)) { + fuzziness = Fuzziness.ONE; + } else { + fuzziness = Fuzziness.ZERO; + } + break; + case 4: + analyzer = analyzer == null ? randomAlphaOfLength(5) : null; + break; + case 5: + useField = useField == null ? randomAlphaOfLength(5) : null; + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + return new Fuzzy( + term, + prefixLength, + isTranspositions, + fuzziness, + analyzer, + useField + ); + } + + @Override + protected Writeable.Reader instanceReader() { + return Fuzzy::new; + } + + @Override + protected Fuzzy doParseInstance(XContentParser parser) throws IOException { + if (parser.nextToken() == XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + Fuzzy Fuzzy = (Fuzzy) IntervalsSourceProvider.fromXContent(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + return Fuzzy; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java index ed7caeb0473de..4da5ebf4d4ea7 100644 --- a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java @@ -89,47 +89,61 @@ protected void initializeAdditionalMappings(MapperService mapperService) throws new CompressedXContent(Strings.toString(mapping)), MapperService.MergeReason.MAPPING_UPDATE); } - private IntervalsSourceProvider createRandomSource(int depth, boolean useScripts) { + private static IntervalsSourceProvider createRandomSource(int depth, boolean useScripts) { if (depth > 2) { return createRandomMatch(depth + 1, useScripts); } switch (randomInt(20)) { case 0: case 1: - int orCount = randomInt(4) + 1; - List orSources = new ArrayList<>(); - for (int i = 0; i < orCount; i++) { - orSources.add(createRandomSource(depth + 1, useScripts)); - } - return new IntervalsSourceProvider.Disjunction(orSources, createRandomFilter(depth + 1, useScripts)); + return createRandomDisjunction(depth, useScripts); case 2: case 3: - int count = randomInt(5) + 1; - List subSources = new ArrayList<>(); - for (int i = 0; i < count; i++) { - subSources.add(createRandomSource(depth + 1, useScripts)); - } - boolean ordered = randomBoolean(); - int maxGaps = randomInt(5) - 1; - IntervalsSourceProvider.IntervalFilter filter = createRandomFilter(depth + 1, useScripts); - return new IntervalsSourceProvider.Combine(subSources, ordered, maxGaps, filter); + return createRandomCombine(depth, useScripts); default: return createRandomMatch(depth + 1, useScripts); } } - private IntervalsSourceProvider.IntervalFilter createRandomFilter(int depth, boolean useScripts) { + static IntervalsSourceProvider.Disjunction createRandomDisjunction(int depth, boolean useScripts) { + int orCount = randomInt(4) + 1; + List orSources = createRandomSourceList(depth, useScripts, orCount); + return new IntervalsSourceProvider.Disjunction(orSources, createRandomFilter(depth + 1, useScripts)); + } + + static IntervalsSourceProvider.Combine createRandomCombine(int depth, boolean useScripts) { + int count = randomInt(5) + 1; + List subSources = createRandomSourceList(depth, useScripts, count); + boolean ordered = randomBoolean(); + int maxGaps = randomInt(5) - 1; + IntervalsSourceProvider.IntervalFilter filter = createRandomFilter(depth + 1, useScripts); + return new IntervalsSourceProvider.Combine(subSources, ordered, maxGaps, filter); + } + + static List createRandomSourceList(int depth, boolean useScripts, int count) { + List subSources = new ArrayList<>(); + for (int i = 0; i < count; i++) { + subSources.add(createRandomSource(depth + 1, useScripts)); + } + return subSources; + } + + private static IntervalsSourceProvider.IntervalFilter createRandomFilter(int depth, boolean useScripts) { if (depth < 3 && randomInt(20) > 18) { - if (useScripts == false || randomBoolean()) { - return new IntervalsSourceProvider.IntervalFilter(createRandomSource(depth + 1, false), randomFrom(filters)); - } - return new IntervalsSourceProvider.IntervalFilter( - new Script(ScriptType.INLINE, "mockscript", "1", Collections.emptyMap())); + return createRandomNonNullFilter(depth, useScripts); } return null; } - private IntervalsSourceProvider createRandomMatch(int depth, boolean useScripts) { + static IntervalsSourceProvider.IntervalFilter createRandomNonNullFilter(int depth, boolean useScripts) { + if (useScripts == false || randomBoolean()) { + return new IntervalsSourceProvider.IntervalFilter(createRandomSource(depth + 1, false), randomFrom(filters)); + } + return new IntervalsSourceProvider.IntervalFilter( + new Script(ScriptType.INLINE, "mockscript", "1", Collections.emptyMap())); + } + + static IntervalsSourceProvider.Match createRandomMatch(int depth, boolean useScripts) { String useField = rarely() ? MASKED_FIELD : null; int wordCount = randomInt(4) + 1; List words = new ArrayList<>(); diff --git a/server/src/test/java/org/elasticsearch/index/query/MatchIntervalsSourceProviderTests.java b/server/src/test/java/org/elasticsearch/index/query/MatchIntervalsSourceProviderTests.java new file mode 100644 index 0000000000000..be2a2d69a05e8 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/MatchIntervalsSourceProviderTests.java @@ -0,0 +1,93 @@ +/* + * 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. + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; + +import static org.elasticsearch.index.query.IntervalsSourceProvider.Match; + +public class MatchIntervalsSourceProviderTests extends AbstractSerializingTestCase { + + @Override + protected Match createTestInstance() { + return IntervalQueryBuilderTests.createRandomMatch(0, randomBoolean()); + } + + @Override + protected Match mutateInstance(Match instance) throws IOException { + String query = instance.getQuery(); + int maxGaps = instance.getMaxGaps(); + boolean isOrdered = instance.isOrdered(); + String analyzer = instance.getAnalyzer(); + IntervalsSourceProvider.IntervalFilter filter = instance.getFilter(); + String useField = instance.getUseField(); + switch (between(0, 5)) { + case 0: + query = randomAlphaOfLength(query.length() + 3); + break; + case 1: + maxGaps++; + break; + case 2: + isOrdered = !isOrdered; + break; + case 3: + analyzer = analyzer == null ? randomAlphaOfLength(5) : null; + break; + case 4: + filter = filter == null ? + IntervalQueryBuilderTests.createRandomNonNullFilter(0, randomBoolean()) : + FilterIntervalsSourceProviderTests.mutateFilter(filter); + break; + case 5: + useField = useField == null ? randomAlphaOfLength(5) : (useField + "foo"); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + return new Match(query, maxGaps, isOrdered, analyzer, filter, useField); + } + + @Override + protected Writeable.Reader instanceReader() { + return Match::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(SearchModule.getIntervalsSourceProviderNamedWritables()); + } + + @Override + protected Match doParseInstance(XContentParser parser) throws IOException { + if (parser.nextToken() == XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + Match Match = (Match) IntervalsSourceProvider.fromXContent(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + return Match; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/query/WildcardIntervalsSourceProviderTests.java b/server/src/test/java/org/elasticsearch/index/query/WildcardIntervalsSourceProviderTests.java index 7bcf3defeea9c..b67621e8878f1 100644 --- a/server/src/test/java/org/elasticsearch/index/query/WildcardIntervalsSourceProviderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/WildcardIntervalsSourceProviderTests.java @@ -31,6 +31,10 @@ public class WildcardIntervalsSourceProviderTests extends AbstractSerializingTes @Override protected Wildcard createTestInstance() { + return createRandomWildcard(); + } + + static Wildcard createRandomWildcard() { return new Wildcard( randomAlphaOfLength(10), randomBoolean() ? randomAlphaOfLength(10) : null, From 5d15e90efaad086295ce77c71cd63322dfdd41e0 Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Mon, 6 Jan 2020 13:24:07 +0100 Subject: [PATCH 390/686] Deleted docs disregarded for if_seq_no check (#50526) Previously, as long as a deleted version value was kept as a tombstone, another index or delete operation against the same id would leak that the doc had existed (through seq_no info) or would allow the operation if the client forged the seq_no. Fixed to disregard info on deleted docs when doing seq_no based optimistic concurrency check. --- .../index/engine/InternalEngine.java | 4 +- .../index/engine/InternalEngineTests.java | 38 +++++++++++++++++-- .../indices/settings/UpdateSettingsIT.java | 22 +++++------ .../versioning/SimpleVersioningIT.java | 9 +---- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index a090501ca0de4..0ea50322b173b 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -1032,7 +1032,7 @@ private IndexingStrategy planIndexingAsPrimary(Index index) throws IOException { currentVersion = versionValue.version; currentNotFoundOrDeleted = versionValue.isDelete(); } - if (index.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && versionValue == null) { + if (index.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && currentNotFoundOrDeleted) { final VersionConflictEngineException e = new VersionConflictEngineException(shardId, index.id(), index.getIfSeqNo(), index.getIfPrimaryTerm(), SequenceNumbers.UNASSIGNED_SEQ_NO, SequenceNumbers.UNASSIGNED_PRIMARY_TERM); @@ -1361,7 +1361,7 @@ private DeletionStrategy planDeletionAsPrimary(Delete delete) throws IOException currentlyDeleted = versionValue.isDelete(); } final DeletionStrategy plan; - if (delete.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && versionValue == null) { + if (delete.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && currentlyDeleted) { final VersionConflictEngineException e = new VersionConflictEngineException(shardId, delete.id(), delete.getIfSeqNo(), delete.getIfPrimaryTerm(), SequenceNumbers.UNASSIGNED_SEQ_NO, SequenceNumbers.UNASSIGNED_PRIMARY_TERM); plan = DeletionStrategy.skipDueToVersionConflict(e, currentVersion, true); diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index b9fe4c7dc4d98..c71730b6494f9 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -138,6 +138,7 @@ import org.elasticsearch.test.VersionUtils; import org.elasticsearch.threadpool.ThreadPool; import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; import java.io.Closeable; import java.io.IOException; @@ -1719,7 +1720,7 @@ private int assertOpsOnPrimary(List ops, long currentOpVersion currentTerm.set(currentTerm.get() + 1L); engine.rollTranslogGeneration(); } - final long correctVersion = docDeleted && randomBoolean() ? Versions.MATCH_DELETED : lastOpVersion; + final long correctVersion = docDeleted ? Versions.MATCH_DELETED : lastOpVersion; logger.info("performing [{}]{}{}", op.operationType().name().charAt(0), versionConflict ? " (conflict " + conflictingVersion + ")" : "", @@ -1742,7 +1743,7 @@ private int assertOpsOnPrimary(List ops, long currentOpVersion final Engine.IndexResult result; if (versionedOp) { // TODO: add support for non-existing docs - if (randomBoolean() && lastOpSeqNo != SequenceNumbers.UNASSIGNED_SEQ_NO) { + if (randomBoolean() && lastOpSeqNo != SequenceNumbers.UNASSIGNED_SEQ_NO && docDeleted == false) { result = engine.index(indexWithSeq.apply(lastOpSeqNo, lastOpTerm, index)); } else { result = engine.index(indexWithVersion.apply(correctVersion, index)); @@ -1777,8 +1778,9 @@ private int assertOpsOnPrimary(List ops, long currentOpVersion assertThat(result.getFailure(), instanceOf(VersionConflictEngineException.class)); } else { final Engine.DeleteResult result; + long correctSeqNo = docDeleted ? UNASSIGNED_SEQ_NO : lastOpSeqNo; if (versionedOp && lastOpSeqNo != UNASSIGNED_SEQ_NO && randomBoolean()) { - result = engine.delete(delWithSeq.apply(lastOpSeqNo, lastOpTerm, delete)); + result = engine.delete(delWithSeq.apply(correctSeqNo, lastOpTerm, delete)); } else if (versionedOp) { result = engine.delete(delWithVersion.apply(correctVersion, delete)); } else { @@ -4031,6 +4033,36 @@ public void testOutOfOrderSequenceNumbersWithVersionConflict() throws IOExceptio } } + /** + * Test that we do not leak out information on a deleted doc due to it existing in version map. There are at least 2 cases: + *
      + *
    • Guessing the deleted seqNo makes the operation succeed
    • + *
    • Providing any other seqNo leaks info that the doc was deleted (and its SeqNo)
    • + *
    + */ + public void testVersionConflictIgnoreDeletedDoc() throws IOException { + ParsedDocument doc = testParsedDocument("1", null, testDocument(), + new BytesArray("{}".getBytes(Charset.defaultCharset())), null); + engine.delete(new Engine.Delete("1", newUid("1"), 1)); + for (long seqNo : new long[]{0, 1, randomNonNegativeLong()}) { + assertDeletedVersionConflict(engine.index(new Engine.Index(newUid("1"), doc, UNASSIGNED_SEQ_NO, 1, + Versions.MATCH_ANY, VersionType.INTERNAL, + PRIMARY, randomNonNegativeLong(), IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, seqNo, 1)), + "update: " + seqNo); + + assertDeletedVersionConflict(engine.delete(new Engine.Delete("1", newUid("1"), UNASSIGNED_SEQ_NO, 1, + Versions.MATCH_ANY, VersionType.INTERNAL, PRIMARY, randomNonNegativeLong(), seqNo, 1)), + "delete: " + seqNo); + } + } + + private void assertDeletedVersionConflict(Engine.Result result, String operation) { + assertNotNull("Must have failure for " + operation, result.getFailure()); + assertThat(operation, result.getFailure(), Matchers.instanceOf(VersionConflictEngineException.class)); + VersionConflictEngineException exception = (VersionConflictEngineException) result.getFailure(); + assertThat(operation, exception.getMessage(), containsString("but no document was found")); + } + /* * This test tests that a no-op does not generate a new sequence number, that no-ops can advance the local checkpoint, and that no-ops * are correctly added to the translog. diff --git a/server/src/test/java/org/elasticsearch/indices/settings/UpdateSettingsIT.java b/server/src/test/java/org/elasticsearch/indices/settings/UpdateSettingsIT.java index 3d063ac3690bb..e79c0a15d21c9 100644 --- a/server/src/test/java/org/elasticsearch/indices/settings/UpdateSettingsIT.java +++ b/server/src/test/java/org/elasticsearch/indices/settings/UpdateSettingsIT.java @@ -21,13 +21,13 @@ import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; import org.elasticsearch.action.admin.indices.settings.get.GetSettingsResponse; -import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Priority; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexModule; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.VersionType; import org.elasticsearch.index.engine.VersionConflictEngineException; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.plugins.Plugin; @@ -449,16 +449,15 @@ public void testOpenCloseUpdateSettings() throws Exception { public void testEngineGCDeletesSetting() throws Exception { createIndex("test"); - client().prepareIndex("test").setId("1").setSource("f", 1).get(); - DeleteResponse response = client().prepareDelete("test", "1").get(); - long seqNo = response.getSeqNo(); - long primaryTerm = response.getPrimaryTerm(); - // delete is still in cache this should work - client().prepareIndex("test").setId("1").setSource("f", 2).setIfSeqNo(seqNo).setIfPrimaryTerm(primaryTerm).get(); + client().prepareIndex("test").setId("1").setSource("f", 1).setVersionType(VersionType.EXTERNAL).setVersion(1).get(); + client().prepareDelete("test", "1").setVersionType(VersionType.EXTERNAL).setVersion(2).get(); + // delete is still in cache this should fail + assertThrows(client().prepareIndex("test").setId("1").setSource("f", 3).setVersionType(VersionType.EXTERNAL).setVersion(1), + VersionConflictEngineException.class); + assertAcked(client().admin().indices().prepareUpdateSettings("test").setSettings(Settings.builder().put("index.gc_deletes", 0))); - response = client().prepareDelete("test", "1").get(); - seqNo = response.getSeqNo(); + client().prepareDelete("test", "1").setVersionType(VersionType.EXTERNAL).setVersion(4).get(); // Make sure the time has advanced for InternalEngine#resolveDocVersion() for (ThreadPool threadPool : internalCluster().getInstances(ThreadPool.class)) { @@ -466,9 +465,8 @@ public void testEngineGCDeletesSetting() throws Exception { assertBusy(() -> assertThat(threadPool.relativeTimeInMillis(), greaterThan(startTime))); } - // delete is should not be in cache - assertThrows(client().prepareIndex("test").setId("1").setSource("f", 3).setIfSeqNo(seqNo).setIfPrimaryTerm(primaryTerm), - VersionConflictEngineException.class); + // delete should not be in cache + client().prepareIndex("test").setId("1").setSource("f", 2).setVersionType(VersionType.EXTERNAL).setVersion(1); } public void testUpdateSettingsWithBlocks() { diff --git a/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java b/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java index 9acc0c572039a..7709100cca2b7 100644 --- a/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java +++ b/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java @@ -283,13 +283,8 @@ public void testCompareAndSet() { assertThrows(client().prepareDelete("test", "1").setIfSeqNo(3).setIfPrimaryTerm(12), VersionConflictEngineException.class); assertThrows(client().prepareDelete("test", "1").setIfSeqNo(1).setIfPrimaryTerm(2), VersionConflictEngineException.class); - - // This is intricate - the object was deleted but a delete transaction was with the right version. We add another one - // and thus the transaction is increased. - deleteResponse = client().prepareDelete("test", "1").setIfSeqNo(2).setIfPrimaryTerm(1).get(); - assertEquals(DocWriteResponse.Result.NOT_FOUND, deleteResponse.getResult()); - assertThat(deleteResponse.getSeqNo(), equalTo(3L)); - assertThat(deleteResponse.getPrimaryTerm(), equalTo(1L)); + // the doc is deleted. Even when we hit the deleted seqNo, a conditional delete should fail. + assertThrows(client().prepareDelete("test", "1").setIfSeqNo(2).setIfPrimaryTerm(1), VersionConflictEngineException.class); } public void testSimpleVersioningWithFlush() throws Exception { From 0158b153832a540601f721dd51eb1c1f0a281453 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 6 Jan 2020 08:25:00 -0500 Subject: [PATCH 391/686] Replace bespoke parser for significance heuristics (#50623) This replaces the hand written xcontent parsers for significance heristics with `ObjectParser` and parsing named xcontent. As a happy accident, this was the last user of `ParseFieldRegistry` so this PR entirely removes that class. Closes #25519 --- .../common/xcontent/ParseFieldRegistry.java | 107 ------------------ .../elasticsearch/plugins/SearchPlugin.java | 17 ++- .../java/org/elasticsearch/script/Script.java | 14 +++ .../elasticsearch/search/SearchModule.java | 41 +++---- .../SignificantTermsAggregationBuilder.java | 49 +++----- .../SignificantTextAggregationBuilder.java | 32 ++---- .../significant/heuristics/ChiSquare.java | 14 +-- .../bucket/significant/heuristics/GND.java | 39 ++----- .../significant/heuristics/JLHScore.java | 20 +--- .../heuristics/MutualInformation.java | 14 +-- .../heuristics/NXYSignificanceHeuristic.java | 50 ++++---- .../heuristics/PercentageScore.java | 2 + .../heuristics/ScriptHeuristic.java | 37 ++---- .../SignificanceHeuristicParser.java | 34 ------ .../search/SearchModuleTests.java | 6 +- .../SignificantTermsSignificanceScoreIT.java | 16 +-- .../SignificanceHeuristicTests.java | 72 +++++------- 17 files changed, 160 insertions(+), 404 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/common/xcontent/ParseFieldRegistry.java delete mode 100644 server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/SignificanceHeuristicParser.java diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/ParseFieldRegistry.java b/server/src/main/java/org/elasticsearch/common/xcontent/ParseFieldRegistry.java deleted file mode 100644 index 98ecf52e4814d..0000000000000 --- a/server/src/main/java/org/elasticsearch/common/xcontent/ParseFieldRegistry.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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. - */ - -package org.elasticsearch.common.xcontent; - -import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.ParsingException; -import org.elasticsearch.common.collect.Tuple; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * Registry for looking things up using ParseField semantics. - */ -public class ParseFieldRegistry { - private final Map> registry = new HashMap<>(); - private final String registryName; - - /** - * Build the registry. - * @param registryName used for error messages - */ - public ParseFieldRegistry(String registryName) { - this.registryName = registryName; - } - - /** - * All the names under which values are registered. Expect this to be used mostly for testing. - */ - public Set getNames() { - return registry.keySet(); - } - - /** - * Register a parser. - */ - public void register(T value, String name) { - register(value, new ParseField(name)); - } - - /** - * Register a parser. - */ - public void register(T value, ParseField parseField) { - Tuple parseFieldParserTuple = new Tuple<>(parseField, value); - for (String name: parseField.getAllNamesIncludedDeprecated()) { - Tuple previousValue = registry.putIfAbsent(name, parseFieldParserTuple); - if (previousValue != null) { - throw new IllegalArgumentException("[" + previousValue.v2() + "] already registered for [" + registryName + "][" + name - + "] while trying to register [" + value + "]"); - } - } - } - - /** - * Lookup a value from the registry by name while checking that the name matches the ParseField. - * - * @param name The name of the thing to look up. - * @return The value being looked up. Never null. - * @throws ParsingException if the named thing isn't in the registry or the name was deprecated and deprecated names aren't supported. - */ - public T lookup(String name, XContentLocation xContentLocation, DeprecationHandler deprecationHandler) { - T value = lookupReturningNullIfNotFound(name, deprecationHandler); - if (value == null) { - throw new ParsingException(xContentLocation, "no [" + registryName + "] registered for [" + name + "]"); - } - return value; - } - - /** - * Lookup a value from the registry by name while checking that the name matches the ParseField. - * - * @param name The name of the thing to look up. - * @return The value being looked up or null if it wasn't found. - * @throws ParsingException if the named thing isn't in the registry or the name was deprecated and deprecated names aren't supported. - */ - public T lookupReturningNullIfNotFound(String name, DeprecationHandler deprecationHandler) { - Tuple parseFieldAndValue = registry.get(name); - if (parseFieldAndValue == null) { - return null; - } - ParseField parseField = parseFieldAndValue.v1(); - T value = parseFieldAndValue.v2(); - boolean match = parseField.match(name, deprecationHandler); - //this is always expected to match, ParseField is useful for deprecation warnings etc. here - assert match : "ParseField did not match registered name [" + name + "][" + registryName + "]"; - return value; - } -} diff --git a/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java b/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java index 9cd3f391a2bd5..b48f1cbcc3f1c 100644 --- a/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java @@ -40,7 +40,6 @@ import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.significant.SignificantTerms; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; -import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicParser; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.elasticsearch.search.fetch.FetchSubPhase; import org.elasticsearch.search.fetch.subphase.highlight.Highlighter; @@ -54,6 +53,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.function.BiFunction; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; @@ -72,7 +72,7 @@ default List> getScoreFunctions() { * The new {@link SignificanceHeuristic}s defined by this plugin. {@linkplain SignificanceHeuristic}s are used by the * {@link SignificantTerms} aggregation to pick which terms are significant for a given query. */ - default List> getSignificanceHeuristics() { + default List> getSignificanceHeuristics() { return emptyList(); } /** @@ -137,6 +137,19 @@ public ScoreFunctionSpec(String name, Writeable.Reader reader, ScoreFunctionP } } + /** + * Specification of custom {@link SignificanceHeuristic}. + */ + class SignificanceHeuristicSpec extends SearchExtensionSpec> { + public SignificanceHeuristicSpec(ParseField name, Writeable.Reader reader, BiFunction parser) { + super(name, reader, parser); + } + + public SignificanceHeuristicSpec(String name, Writeable.Reader reader, BiFunction parser) { + super(name, reader, parser); + } + } + /** * Specification for a {@link Suggester}. */ diff --git a/server/src/main/java/org/elasticsearch/script/Script.java b/server/src/main/java/org/elasticsearch/script/Script.java index bd74ed806cd74..dc5dd600ba2d3 100644 --- a/server/src/main/java/org/elasticsearch/script/Script.java +++ b/server/src/main/java/org/elasticsearch/script/Script.java @@ -27,6 +27,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.AbstractObjectParser; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ObjectParser; @@ -47,6 +48,9 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.function.BiConsumer; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; /** * {@link Script} represents used-defined input that can be used to @@ -269,6 +273,16 @@ private Script build(String defaultLang) { PARSER.declareField(Builder::setParams, XContentParser::map, PARAMS_PARSE_FIELD, ValueType.OBJECT); } + /** + * Declare a script field on an {@link ObjectParser}. + * @param Whatever type the {@linkplain ObjectParser} is parsing. + * @param parser the parser itself + * @param consumer the consumer for the script + */ + public static void declareScript(AbstractObjectParser parser, BiConsumer consumer) { + parser.declareField(constructorArg(), (p, c) -> Script.parse(p), Script.SCRIPT_PARSE_FIELD, ValueType.OBJECT_OR_STRING); + } + /** * Convenience method to call {@link Script#parse(XContentParser, String)} * using the default scripting language. diff --git a/server/src/main/java/org/elasticsearch/search/SearchModule.java b/server/src/main/java/org/elasticsearch/search/SearchModule.java index 0be9317242c68..6e861889c7bc6 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/server/src/main/java/org/elasticsearch/search/SearchModule.java @@ -29,7 +29,6 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; -import org.elasticsearch.common.xcontent.ParseFieldRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.BoostingQueryBuilder; @@ -66,6 +65,7 @@ import org.elasticsearch.index.query.SpanFirstQueryBuilder; import org.elasticsearch.index.query.SpanMultiTermQueryBuilder; import org.elasticsearch.index.query.SpanNearQueryBuilder; +import org.elasticsearch.index.query.SpanNearQueryBuilder.SpanGapQueryBuilder; import org.elasticsearch.index.query.SpanNotQueryBuilder; import org.elasticsearch.index.query.SpanOrQueryBuilder; import org.elasticsearch.index.query.SpanTermQueryBuilder; @@ -93,7 +93,7 @@ import org.elasticsearch.plugins.SearchPlugin.RescorerSpec; import org.elasticsearch.plugins.SearchPlugin.ScoreFunctionSpec; import org.elasticsearch.plugins.SearchPlugin.SearchExtSpec; -import org.elasticsearch.plugins.SearchPlugin.SearchExtensionSpec; +import org.elasticsearch.plugins.SearchPlugin.SignificanceHeuristicSpec; import org.elasticsearch.plugins.SearchPlugin.SuggesterSpec; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregatorFactories; @@ -150,7 +150,6 @@ import org.elasticsearch.search.aggregations.bucket.significant.heuristics.PercentageScore; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.ScriptHeuristic; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; -import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicParser; import org.elasticsearch.search.aggregations.bucket.terms.DoubleTerms; import org.elasticsearch.search.aggregations.bucket.terms.LongRareTerms; import org.elasticsearch.search.aggregations.bucket.terms.LongTerms; @@ -273,7 +272,6 @@ import static java.util.Collections.unmodifiableMap; import static java.util.Objects.requireNonNull; -import static org.elasticsearch.index.query.SpanNearQueryBuilder.SpanGapQueryBuilder; /** * Sets up things that can be done at search time like queries, aggregations, and suggesters. @@ -283,8 +281,6 @@ public class SearchModule { 1024, 1, Integer.MAX_VALUE, Setting.Property.NodeScope); private final Map highlighters; - private final ParseFieldRegistry significanceHeuristicParserRegistry = new ParseFieldRegistry<>( - "significance_heuristic"); private final List fetchSubPhases = new ArrayList<>(); @@ -333,13 +329,6 @@ public Map getHighlighters() { return highlighters; } - /** - * The registry of {@link SignificanceHeuristic}s. - */ - public ParseFieldRegistry getSignificanceHeuristicParserRegistry() { - return significanceHeuristicParserRegistry; - } - private void registerAggregations(List plugins) { registerAggregation(new AggregationSpec(AvgAggregationBuilder.NAME, AvgAggregationBuilder::new, AvgAggregationBuilder::parse) .addResultReader(InternalAvg::new)); @@ -399,12 +388,12 @@ private void registerAggregations(List plugins) { .addResultReader(UnmappedRareTerms.NAME, UnmappedRareTerms::new) .addResultReader(LongRareTerms.NAME, LongRareTerms::new)); registerAggregation(new AggregationSpec(SignificantTermsAggregationBuilder.NAME, SignificantTermsAggregationBuilder::new, - SignificantTermsAggregationBuilder.getParser(significanceHeuristicParserRegistry)) + SignificantTermsAggregationBuilder::parse) .addResultReader(SignificantStringTerms.NAME, SignificantStringTerms::new) .addResultReader(SignificantLongTerms.NAME, SignificantLongTerms::new) .addResultReader(UnmappedSignificantTerms.NAME, UnmappedSignificantTerms::new)); registerAggregation(new AggregationSpec(SignificantTextAggregationBuilder.NAME, SignificantTextAggregationBuilder::new, - SignificantTextAggregationBuilder.getParser(significanceHeuristicParserRegistry))); + SignificantTextAggregationBuilder::parse)); registerAggregation(new AggregationSpec(RangeAggregationBuilder.NAME, RangeAggregationBuilder::new, RangeAggregationBuilder::parse).addResultReader(InternalRange::new)); registerAggregation(new AggregationSpec(DateRangeAggregationBuilder.NAME, DateRangeAggregationBuilder::new, @@ -680,20 +669,22 @@ private void registerValueFormat(String name, Writeable.Reader plugins) { - registerSignificanceHeuristic(new SearchExtensionSpec<>(ChiSquare.NAME, ChiSquare::new, ChiSquare.PARSER)); - registerSignificanceHeuristic(new SearchExtensionSpec<>(GND.NAME, GND::new, GND.PARSER)); - registerSignificanceHeuristic(new SearchExtensionSpec<>(JLHScore.NAME, JLHScore::new, JLHScore::parse)); - registerSignificanceHeuristic(new SearchExtensionSpec<>(MutualInformation.NAME, MutualInformation::new, MutualInformation.PARSER)); - registerSignificanceHeuristic(new SearchExtensionSpec<>(PercentageScore.NAME, PercentageScore::new, PercentageScore::parse)); - registerSignificanceHeuristic(new SearchExtensionSpec<>(ScriptHeuristic.NAME, ScriptHeuristic::new, ScriptHeuristic::parse)); + registerSignificanceHeuristic(new SignificanceHeuristicSpec<>(ChiSquare.NAME, ChiSquare::new, ChiSquare.PARSER)); + registerSignificanceHeuristic(new SignificanceHeuristicSpec<>(GND.NAME, GND::new, GND.PARSER)); + registerSignificanceHeuristic(new SignificanceHeuristicSpec<>(JLHScore.NAME, JLHScore::new, JLHScore.PARSER)); + registerSignificanceHeuristic(new SignificanceHeuristicSpec<>( + MutualInformation.NAME, MutualInformation::new, MutualInformation.PARSER)); + registerSignificanceHeuristic(new SignificanceHeuristicSpec<>(PercentageScore.NAME, PercentageScore::new, PercentageScore.PARSER)); + registerSignificanceHeuristic(new SignificanceHeuristicSpec<>(ScriptHeuristic.NAME, ScriptHeuristic::new, ScriptHeuristic.PARSER)); registerFromPlugin(plugins, SearchPlugin::getSignificanceHeuristics, this::registerSignificanceHeuristic); } - private void registerSignificanceHeuristic(SearchExtensionSpec heuristic) { - significanceHeuristicParserRegistry.register(heuristic.getParser(), heuristic.getName()); - namedWriteables.add(new NamedWriteableRegistry.Entry(SignificanceHeuristic.class, heuristic.getName().getPreferredName(), - heuristic.getReader())); + private void registerSignificanceHeuristic(SignificanceHeuristicSpec spec) { + namedXContents.add(new NamedXContentRegistry.Entry( + SignificanceHeuristic.class, spec.getName(), p -> spec.getParser().apply(p, null))); + namedWriteables.add(new NamedWriteableRegistry.Entry( + SignificanceHeuristic.class, spec.getName().getPreferredName(), spec.getReader())); } private void registerFetchSubPhases(List plugins) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregationBuilder.java index 908bc0586169c..81d1b0445b3e2 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregationBuilder.java @@ -23,30 +23,27 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ObjectParser; -import org.elasticsearch.common.xcontent.ParseFieldRegistry; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories.Builder; import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.aggregations.bucket.MultiBucketAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.JLHScore; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; -import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicParser; import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregator; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregator.BucketCountThresholds; +import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.search.aggregations.support.ValueType; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; import org.elasticsearch.search.aggregations.support.ValuesSourceParserHelper; -import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import java.io.IOException; import java.util.Map; @@ -65,48 +62,36 @@ public class SignificantTermsAggregationBuilder extends ValuesSourceAggregationB 3, 0, 10, -1); static final SignificanceHeuristic DEFAULT_SIGNIFICANCE_HEURISTIC = new JLHScore(); - public static Aggregator.Parser getParser(ParseFieldRegistry significanceHeuristicParserRegistry) { - ObjectParser aggregationParser = - new ObjectParser<>(SignificantTermsAggregationBuilder.NAME); - ValuesSourceParserHelper.declareAnyFields(aggregationParser, true, true); + private static final ObjectParser PARSER = new ObjectParser<>( + SignificantTermsAggregationBuilder.NAME, + SignificanceHeuristic.class, SignificantTermsAggregationBuilder::significanceHeuristic, null); + static { + ValuesSourceParserHelper.declareAnyFields(PARSER, true, true); - aggregationParser.declareInt(SignificantTermsAggregationBuilder::shardSize, TermsAggregationBuilder.SHARD_SIZE_FIELD_NAME); + PARSER.declareInt(SignificantTermsAggregationBuilder::shardSize, TermsAggregationBuilder.SHARD_SIZE_FIELD_NAME); - aggregationParser.declareLong(SignificantTermsAggregationBuilder::minDocCount, TermsAggregationBuilder.MIN_DOC_COUNT_FIELD_NAME); + PARSER.declareLong(SignificantTermsAggregationBuilder::minDocCount, TermsAggregationBuilder.MIN_DOC_COUNT_FIELD_NAME); - aggregationParser.declareLong(SignificantTermsAggregationBuilder::shardMinDocCount, + PARSER.declareLong(SignificantTermsAggregationBuilder::shardMinDocCount, TermsAggregationBuilder.SHARD_MIN_DOC_COUNT_FIELD_NAME); - aggregationParser.declareInt(SignificantTermsAggregationBuilder::size, TermsAggregationBuilder.REQUIRED_SIZE_FIELD_NAME); + PARSER.declareInt(SignificantTermsAggregationBuilder::size, TermsAggregationBuilder.REQUIRED_SIZE_FIELD_NAME); - aggregationParser.declareString(SignificantTermsAggregationBuilder::executionHint, + PARSER.declareString(SignificantTermsAggregationBuilder::executionHint, TermsAggregationBuilder.EXECUTION_HINT_FIELD_NAME); - aggregationParser.declareObject(SignificantTermsAggregationBuilder::backgroundFilter, + PARSER.declareObject(SignificantTermsAggregationBuilder::backgroundFilter, (p, context) -> parseInnerQueryBuilder(p), SignificantTermsAggregationBuilder.BACKGROUND_FILTER); - aggregationParser.declareField((b, v) -> b.includeExclude(IncludeExclude.merge(v, b.includeExclude())), + PARSER.declareField((b, v) -> b.includeExclude(IncludeExclude.merge(v, b.includeExclude())), IncludeExclude::parseInclude, IncludeExclude.INCLUDE_FIELD, ObjectParser.ValueType.OBJECT_ARRAY_OR_STRING); - aggregationParser.declareField((b, v) -> b.includeExclude(IncludeExclude.merge(b.includeExclude(), v)), + PARSER.declareField((b, v) -> b.includeExclude(IncludeExclude.merge(b.includeExclude(), v)), IncludeExclude::parseExclude, IncludeExclude.EXCLUDE_FIELD, ObjectParser.ValueType.STRING_ARRAY); - - for (String name : significanceHeuristicParserRegistry.getNames()) { - aggregationParser.declareObject(SignificantTermsAggregationBuilder::significanceHeuristic, - (p, context) -> { - SignificanceHeuristicParser significanceHeuristicParser = significanceHeuristicParserRegistry - .lookupReturningNullIfNotFound(name, p.getDeprecationHandler()); - return significanceHeuristicParser.parse(p); - }, - new ParseField(name)); - } - return new Aggregator.Parser() { - @Override - public AggregationBuilder parse(String aggregationName, XContentParser parser) throws IOException { - return aggregationParser.parse(parser, new SignificantTermsAggregationBuilder(aggregationName, null), null); - } - }; + } + public static SignificantTermsAggregationBuilder parse(String aggregationName, XContentParser parser) throws IOException { + return PARSER.parse(parser, new SignificantTermsAggregationBuilder(aggregationName, null), null); } private IncludeExclude includeExclude = null; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregationBuilder.java index d2e3729c335a5..c3c6e6ea53baa 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregationBuilder.java @@ -23,7 +23,6 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ObjectParser; -import org.elasticsearch.common.xcontent.ParseFieldRegistry; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.AbstractQueryBuilder; @@ -32,11 +31,9 @@ import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationInitializationException; -import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories.Builder; import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; -import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicParser; import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregator; @@ -69,11 +66,10 @@ public class SignificantTextAggregationBuilder extends AbstractAggregationBuilde DEFAULT_BUCKET_COUNT_THRESHOLDS); private SignificanceHeuristic significanceHeuristic = DEFAULT_SIGNIFICANCE_HEURISTIC; - public static Aggregator.Parser getParser( - ParseFieldRegistry significanceHeuristicParserRegistry) { - ObjectParser PARSER = new ObjectParser<>( - SignificantTextAggregationBuilder.NAME); - + private static final ObjectParser PARSER = new ObjectParser<>( + SignificantTextAggregationBuilder.NAME, + SignificanceHeuristic.class, SignificantTextAggregationBuilder::significanceHeuristic, null); + static { PARSER.declareInt(SignificantTextAggregationBuilder::shardSize, TermsAggregationBuilder.SHARD_SIZE_FIELD_NAME); @@ -105,23 +101,9 @@ public static Aggregator.Parser getParser( PARSER.declareField((b, v) -> b.includeExclude(IncludeExclude.merge(b.includeExclude(), v)), IncludeExclude::parseExclude, IncludeExclude.EXCLUDE_FIELD, ObjectParser.ValueType.STRING_ARRAY); - - for (String name : significanceHeuristicParserRegistry.getNames()) { - PARSER.declareObject(SignificantTextAggregationBuilder::significanceHeuristic, - (p, context) -> { - SignificanceHeuristicParser significanceHeuristicParser = significanceHeuristicParserRegistry - .lookupReturningNullIfNotFound(name, p.getDeprecationHandler()); - return significanceHeuristicParser.parse(p); - }, new ParseField(name)); - } - return new Aggregator.Parser() { - @Override - public AggregationBuilder parse(String aggregationName, XContentParser parser) - throws IOException { - return PARSER.parse(parser, - new SignificantTextAggregationBuilder(aggregationName, null), null); - } - }; + } + public static SignificantTextAggregationBuilder parse(String aggregationName, XContentParser parser) throws IOException { + return PARSER.parse(parser, new SignificantTextAggregationBuilder(aggregationName, null), null); } protected SignificantTextAggregationBuilder(SignificantTextAggregationBuilder clone, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ChiSquare.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ChiSquare.java index de3c6c6acd0a3..a724c976f6899 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ChiSquare.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ChiSquare.java @@ -22,12 +22,18 @@ import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; public class ChiSquare extends NXYSignificanceHeuristic { public static final String NAME = "chi_square"; + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + NAME, buildFromParsedArgs(ChiSquare::new)); + static { + NXYSignificanceHeuristic.declareParseFields(PARSER); + } public ChiSquare(boolean includeNegatives, boolean backgroundIsSuperset) { super(includeNegatives, backgroundIsSuperset); @@ -84,15 +90,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - public static final SignificanceHeuristicParser PARSER = new NXYParser() { - @Override - protected SignificanceHeuristic newHeuristic(boolean includeNegatives, boolean backgroundIsSuperset) { - return new ChiSquare(includeNegatives, backgroundIsSuperset); - } - }; - public static class ChiSquareBuilder extends NXYSignificanceHeuristic.NXYBuilder { - public ChiSquareBuilder(boolean includeNegatives, boolean backgroundIsSuperset) { super(includeNegatives, backgroundIsSuperset); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/GND.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/GND.java index 66b45b54f6416..e4968bd489385 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/GND.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/GND.java @@ -21,17 +21,24 @@ package org.elasticsearch.search.aggregations.bucket.significant.heuristics; -import org.elasticsearch.ElasticsearchParseException; 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.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.query.QueryShardException; import java.io.IOException; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + public class GND extends NXYSignificanceHeuristic { public static final String NAME = "gnd"; + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, args -> { + boolean backgroundIsSuperset = args[0] == null ? true : (boolean) args[0]; + return new GND(backgroundIsSuperset); + }); + static { + PARSER.declareBoolean(optionalConstructorArg(), BACKGROUND_IS_SUPERSET); + } public GND(boolean backgroundIsSuperset) { super(true, backgroundIsSuperset); @@ -105,33 +112,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - public static final SignificanceHeuristicParser PARSER = new NXYParser() { - @Override - protected SignificanceHeuristic newHeuristic(boolean includeNegatives, boolean backgroundIsSuperset) { - return new GND(backgroundIsSuperset); - } - - @Override - public SignificanceHeuristic parse(XContentParser parser) throws IOException, QueryShardException { - String givenName = parser.currentName(); - boolean backgroundIsSuperset = true; - XContentParser.Token token = parser.nextToken(); - while (!token.equals(XContentParser.Token.END_OBJECT)) { - if (BACKGROUND_IS_SUPERSET.match(parser.currentName(), parser.getDeprecationHandler())) { - parser.nextToken(); - backgroundIsSuperset = parser.booleanValue(); - } else { - throw new ElasticsearchParseException("failed to parse [{}] significance heuristic. unknown field [{}]", - givenName, parser.currentName()); - } - token = parser.nextToken(); - } - return newHeuristic(true, backgroundIsSuperset); - } - }; - public static class GNDBuilder extends NXYBuilder { - public GNDBuilder(boolean backgroundIsSuperset) { super(true, backgroundIsSuperset); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/JLHScore.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/JLHScore.java index 2627008b47d81..46eda89177ce7 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/JLHScore.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/JLHScore.java @@ -21,17 +21,16 @@ package org.elasticsearch.search.aggregations.bucket.significant.heuristics; -import org.elasticsearch.ElasticsearchParseException; +import java.io.IOException; + import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.query.QueryShardException; - -import java.io.IOException; public class JLHScore extends SignificanceHeuristic { public static final String NAME = "jlh"; + public static final ObjectParser PARSER = new ObjectParser<>(NAME, JLHScore::new); public JLHScore() { } @@ -103,17 +102,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - public static SignificanceHeuristic parse(XContentParser parser) - throws IOException, QueryShardException { - // move to the closing bracket - if (!parser.nextToken().equals(XContentParser.Token.END_OBJECT)) { - throw new ElasticsearchParseException( - "failed to parse [jlh] significance heuristic. expected an empty object, but found [{}] instead", - parser.currentToken()); - } - return new JLHScore(); - } - @Override public boolean equals(Object obj) { if (obj == null || obj.getClass() != getClass()) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/MutualInformation.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/MutualInformation.java index f8c8b61a7293b..c226d48d5d665 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/MutualInformation.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/MutualInformation.java @@ -22,12 +22,18 @@ import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; public class MutualInformation extends NXYSignificanceHeuristic { public static final String NAME = "mutual_information"; + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + NAME, buildFromParsedArgs(MutualInformation::new)); + static { + NXYSignificanceHeuristic.declareParseFields(PARSER); + } private static final double log2 = Math.log(2.0); @@ -118,15 +124,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - public static final SignificanceHeuristicParser PARSER = new NXYParser() { - @Override - protected SignificanceHeuristic newHeuristic(boolean includeNegatives, boolean backgroundIsSuperset) { - return new MutualInformation(includeNegatives, backgroundIsSuperset); - } - }; - public static class MutualInformationBuilder extends NXYBuilder { - public MutualInformationBuilder(boolean includeNegatives, boolean backgroundIsSuperset) { super(includeNegatives, backgroundIsSuperset); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/NXYSignificanceHeuristic.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/NXYSignificanceHeuristic.java index b01bc8c1650ca..e64e14d8567a0 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/NXYSignificanceHeuristic.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/NXYSignificanceHeuristic.java @@ -21,15 +21,17 @@ package org.elasticsearch.search.aggregations.bucket.significant.heuristics; -import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.ParseField; 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.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.query.QueryShardException; import java.io.IOException; +import java.util.function.BiFunction; +import java.util.function.Function; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; public abstract class NXYSignificanceHeuristic extends SignificanceHeuristic { @@ -160,34 +162,24 @@ protected void build(XContentBuilder builder) throws IOException { backgroundIsSuperset); } - public abstract static class NXYParser implements SignificanceHeuristicParser { - - @Override - public SignificanceHeuristic parse(XContentParser parser) - throws IOException, QueryShardException { - String givenName = parser.currentName(); - boolean includeNegatives = false; - boolean backgroundIsSuperset = true; - XContentParser.Token token = parser.nextToken(); - while (!token.equals(XContentParser.Token.END_OBJECT)) { - if (INCLUDE_NEGATIVES_FIELD.match(parser.currentName(), parser.getDeprecationHandler())) { - parser.nextToken(); - includeNegatives = parser.booleanValue(); - } else if (BACKGROUND_IS_SUPERSET.match(parser.currentName(), parser.getDeprecationHandler())) { - parser.nextToken(); - backgroundIsSuperset = parser.booleanValue(); - } else { - throw new ElasticsearchParseException("failed to parse [{}] significance heuristic. unknown field [{}]", - givenName, parser.currentName()); - } - token = parser.nextToken(); - } - return newHeuristic(includeNegatives, backgroundIsSuperset); - } - - protected abstract SignificanceHeuristic newHeuristic(boolean includeNegatives, boolean backgroundIsSuperset); + /** + * Set up and {@linkplain ConstructingObjectParser} to accept the standard arguments for an {@linkplain NXYSignificanceHeuristic}. + */ + protected static void declareParseFields(ConstructingObjectParser parser) { + parser.declareBoolean(optionalConstructorArg(), INCLUDE_NEGATIVES_FIELD); + parser.declareBoolean(optionalConstructorArg(), BACKGROUND_IS_SUPERSET); } + /** + * Adapt a standard two argument ctor into one that consumes a {@linkplain ConstructingObjectParser}'s fields. + */ + protected static Function buildFromParsedArgs(BiFunction ctor) { + return args -> { + boolean includeNegatives = args[0] == null ? false : (boolean) args[0]; + boolean backgroundIsSuperset = args[1] == null ? true : (boolean) args[1]; + return ctor.apply(includeNegatives, backgroundIsSuperset); + }; + } protected abstract static class NXYBuilder implements SignificanceHeuristicBuilder { protected boolean includeNegatives = true; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/PercentageScore.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/PercentageScore.java index 80f1fece739f7..a5c7dc18b7349 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/PercentageScore.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/PercentageScore.java @@ -24,6 +24,7 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryShardException; @@ -32,6 +33,7 @@ public class PercentageScore extends SignificanceHeuristic { public static final String NAME = "percentage"; + public static final ObjectParser PARSER = new ObjectParser<>(NAME, PercentageScore::new); public PercentageScore() { } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java index e90df2ed33a0b..3d1f331862031 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java @@ -21,13 +21,11 @@ package org.elasticsearch.search.aggregations.bucket.significant.heuristics; -import org.elasticsearch.ElasticsearchParseException; 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.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryShardContext; -import org.elasticsearch.index.query.QueryShardException; import org.elasticsearch.script.Script; import org.elasticsearch.script.SignificantTermsHeuristicScoreScript; import org.elasticsearch.search.aggregations.InternalAggregation; @@ -37,8 +35,15 @@ import java.util.Map; import java.util.Objects; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + public class ScriptHeuristic extends SignificanceHeuristic { public static final String NAME = "script_heuristic"; + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, args -> + new ScriptHeuristic((Script) args[0])); + static { + Script.declareScript(PARSER, constructorArg()); + } private final Script script; @@ -153,32 +158,6 @@ public boolean equals(Object obj) { return Objects.equals(script, other.script); } - public static SignificanceHeuristic parse(XContentParser parser) - throws IOException, QueryShardException { - String heuristicName = parser.currentName(); - Script script = null; - XContentParser.Token token; - String currentFieldName = null; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token.equals(XContentParser.Token.FIELD_NAME)) { - currentFieldName = parser.currentName(); - } else { - if (Script.SCRIPT_PARSE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - script = Script.parse(parser); - } else { - throw new ElasticsearchParseException("failed to parse [{}] significance heuristic. unknown object [{}]", - heuristicName, currentFieldName); - } - } - } - - if (script == null) { - throw new ElasticsearchParseException("failed to parse [{}] significance heuristic. no script found in script_heuristic", - heuristicName); - } - return new ScriptHeuristic(script); - } - public final class LongAccessor extends Number { public long value; @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/SignificanceHeuristicParser.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/SignificanceHeuristicParser.java deleted file mode 100644 index bab88555cbbf6..0000000000000 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/SignificanceHeuristicParser.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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. - */ - - -package org.elasticsearch.search.aggregations.bucket.significant.heuristics; - -import org.elasticsearch.common.ParsingException; -import org.elasticsearch.common.xcontent.XContentParser; - -import java.io.IOException; - -/** - * Parses {@link SignificanceHeuristic}s from an {@link XContentParser}. - */ -@FunctionalInterface -public interface SignificanceHeuristicParser { - SignificanceHeuristic parse(XContentParser parser) throws IOException, ParsingException; -} diff --git a/server/src/test/java/org/elasticsearch/search/SearchModuleTests.java b/server/src/test/java/org/elasticsearch/search/SearchModuleTests.java index 5998982c95db3..95f1e0d4af26d 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchModuleTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchModuleTests.java @@ -40,8 +40,6 @@ import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.ChiSquare; -import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; -import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicParser; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.pipeline.AbstractPipelineAggregationBuilder; import org.elasticsearch.search.aggregations.pipeline.DerivativePipelineAggregationBuilder; @@ -120,8 +118,8 @@ public List> getScoreFunctions() { SearchPlugin registersDupeSignificanceHeuristic = new SearchPlugin() { @Override - public List> getSignificanceHeuristics() { - return singletonList(new SearchExtensionSpec<>(ChiSquare.NAME, ChiSquare::new, ChiSquare.PARSER)); + public List> getSignificanceHeuristics() { + return singletonList(new SignificanceHeuristicSpec<>(ChiSquare.NAME, ChiSquare::new, ChiSquare.PARSER)); } }; expectThrows(IllegalArgumentException.class, registryForPlugin(registersDupeSignificanceHeuristic)); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java index 80b67fe850d8d..9e4717cb61991 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java @@ -25,14 +25,13 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.index.query.QueryShardException; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.SearchPlugin; import org.elasticsearch.script.MockScriptPlugin; @@ -49,7 +48,6 @@ import org.elasticsearch.search.aggregations.bucket.significant.heuristics.MutualInformation; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.ScriptHeuristic; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; -import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicParser; import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.test.ESIntegTestCase; @@ -173,9 +171,8 @@ public void testPlugin() throws Exception { public static class CustomSignificanceHeuristicPlugin extends MockScriptPlugin implements SearchPlugin { @Override - public List> getSignificanceHeuristics() { - return singletonList(new SearchExtensionSpec(SimpleHeuristic.NAME, - SimpleHeuristic::new, (parser) -> SimpleHeuristic.parse(parser))); + public List> getSignificanceHeuristics() { + return singletonList(new SignificanceHeuristicSpec<>(SimpleHeuristic.NAME, SimpleHeuristic::new, SimpleHeuristic.PARSER)); } @Override @@ -209,6 +206,7 @@ private static long longValue(Object value) { public static class SimpleHeuristic extends SignificanceHeuristic { public static final String NAME = "simple"; + public static final ObjectParser PARSER = new ObjectParser<>(NAME, SimpleHeuristic::new); public SimpleHeuristic() { } @@ -263,12 +261,6 @@ public boolean equals(Object obj) { public double getScore(long subsetFreq, long subsetSize, long supersetFreq, long supersetSize) { return subsetFreq / subsetSize > supersetFreq / supersetSize ? 2.0 : 1.0; } - - public static SignificanceHeuristic parse(XContentParser parser) - throws IOException, QueryShardException { - parser.nextToken(); - return new SimpleHeuristic(); - } } public void testXContentResponse() throws Exception { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java index 6afc5e94e3029..510d06579205c 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java @@ -28,7 +28,7 @@ import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.ParseFieldRegistry; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParseException; @@ -46,7 +46,6 @@ import org.elasticsearch.search.aggregations.bucket.significant.heuristics.MutualInformation; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.PercentageScore; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; -import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicParser; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.TestSearchContext; @@ -218,81 +217,62 @@ SignificantLongTerms.Bucket createBucket(long subsetDF, long subsetSize, long su // 1. The output of the builders can actually be parsed // 2. The parser does not swallow parameters after a significance heuristic was defined public void testBuilderAndParser() throws Exception { - SearchModule searchModule = new SearchModule(Settings.EMPTY, emptyList()); - ParseFieldRegistry heuristicParserMapper = searchModule.getSignificanceHeuristicParserRegistry(); - // test jlh with string - assertTrue(parseFromString(heuristicParserMapper, "\"jlh\":{}") instanceof JLHScore); + assertTrue(parseFromString("\"jlh\":{}") instanceof JLHScore); // test gnd with string - assertTrue(parseFromString(heuristicParserMapper, "\"gnd\":{}") instanceof GND); + assertTrue(parseFromString("\"gnd\":{}") instanceof GND); // test mutual information with string boolean includeNegatives = randomBoolean(); boolean backgroundIsSuperset = randomBoolean(); String mutual = "\"mutual_information\":{\"include_negatives\": " + includeNegatives + ", \"background_is_superset\":" + backgroundIsSuperset + "}"; assertEquals(new MutualInformation(includeNegatives, backgroundIsSuperset), - parseFromString(heuristicParserMapper, mutual)); + parseFromString(mutual)); String chiSquare = "\"chi_square\":{\"include_negatives\": " + includeNegatives + ", \"background_is_superset\":" + backgroundIsSuperset + "}"; assertEquals(new ChiSquare(includeNegatives, backgroundIsSuperset), - parseFromString(heuristicParserMapper, chiSquare)); + parseFromString(chiSquare)); // test with builders - assertThat(parseFromBuilder(heuristicParserMapper, new JLHScore()), instanceOf(JLHScore.class)); - assertThat(parseFromBuilder(heuristicParserMapper, new GND(backgroundIsSuperset)), instanceOf(GND.class)); + assertThat(parseFromBuilder(new JLHScore()), instanceOf(JLHScore.class)); + assertThat(parseFromBuilder(new GND(backgroundIsSuperset)), instanceOf(GND.class)); assertEquals(new MutualInformation(includeNegatives, backgroundIsSuperset), - parseFromBuilder(heuristicParserMapper, new MutualInformation(includeNegatives, backgroundIsSuperset))); + parseFromBuilder(new MutualInformation(includeNegatives, backgroundIsSuperset))); assertEquals(new ChiSquare(includeNegatives, backgroundIsSuperset), - parseFromBuilder(heuristicParserMapper, new ChiSquare(includeNegatives, backgroundIsSuperset))); + parseFromBuilder(new ChiSquare(includeNegatives, backgroundIsSuperset))); // test exceptions - String faultyHeuristicdefinition = "\"mutual_information\":{\"include_negatives\": false, \"some_unknown_field\": false}"; - String expectedError = "unknown field [some_unknown_field]"; - checkParseException(heuristicParserMapper, faultyHeuristicdefinition, expectedError); - - faultyHeuristicdefinition = "\"chi_square\":{\"unknown_field\": true}"; - expectedError = "unknown field [unknown_field]"; - checkParseException(heuristicParserMapper, faultyHeuristicdefinition, expectedError); - - faultyHeuristicdefinition = "\"jlh\":{\"unknown_field\": true}"; - expectedError = "expected an empty object, but found "; - checkParseException(heuristicParserMapper, faultyHeuristicdefinition, expectedError); - - faultyHeuristicdefinition = "\"gnd\":{\"unknown_field\": true}"; - expectedError = "unknown field [unknown_field]"; - checkParseException(heuristicParserMapper, faultyHeuristicdefinition, expectedError); + String expectedError = "unknown field [unknown_field]"; + checkParseException("\"mutual_information\":{\"include_negatives\": false, \"unknown_field\": false}", expectedError); + checkParseException("\"chi_square\":{\"unknown_field\": true}", expectedError); + checkParseException("\"jlh\":{\"unknown_field\": true}", expectedError); + checkParseException("\"gnd\":{\"unknown_field\": true}", expectedError); } - protected void checkParseException(ParseFieldRegistry significanceHeuristicParserRegistry, - String faultyHeuristicDefinition, String expectedError) throws IOException { + protected void checkParseException(String faultyHeuristicDefinition, String expectedError) throws IOException { try (XContentParser stParser = createParser(JsonXContent.jsonXContent, "{\"field\":\"text\", " + faultyHeuristicDefinition + ",\"min_doc_count\":200}")) { stParser.nextToken(); - SignificantTermsAggregationBuilder.getParser(significanceHeuristicParserRegistry).parse("testagg", stParser); + SignificantTermsAggregationBuilder.parse("testagg", stParser); fail(); } catch (XContentParseException e) { - assertThat(e.getCause().getMessage(), containsString(expectedError)); + assertThat(e.getMessage(), containsString(expectedError)); } } - protected SignificanceHeuristic parseFromBuilder(ParseFieldRegistry significanceHeuristicParserRegistry, - SignificanceHeuristic significanceHeuristic) throws IOException { + protected SignificanceHeuristic parseFromBuilder(SignificanceHeuristic significanceHeuristic) throws IOException { SignificantTermsAggregationBuilder stBuilder = significantTerms("testagg"); stBuilder.significanceHeuristic(significanceHeuristic).field("text").minDocCount(200); XContentBuilder stXContentBuilder = XContentFactory.jsonBuilder(); stBuilder.internalXContent(stXContentBuilder, null); XContentParser stParser = createParser(JsonXContent.jsonXContent, Strings.toString(stXContentBuilder)); - return parseSignificanceHeuristic(significanceHeuristicParserRegistry, stParser); + return parseSignificanceHeuristic(stParser); } - private static SignificanceHeuristic parseSignificanceHeuristic( - ParseFieldRegistry significanceHeuristicParserRegistry, - XContentParser stParser) throws IOException { + private static SignificanceHeuristic parseSignificanceHeuristic(XContentParser stParser) throws IOException { stParser.nextToken(); - SignificantTermsAggregationBuilder aggregatorFactory = - (SignificantTermsAggregationBuilder) SignificantTermsAggregationBuilder.getParser( - significanceHeuristicParserRegistry).parse("testagg", stParser); + SignificantTermsAggregationBuilder aggregatorFactory = SignificantTermsAggregationBuilder.parse("testagg", stParser); stParser.nextToken(); assertThat(aggregatorFactory.getBucketCountThresholds().getMinDocCount(), equalTo(200L)); assertThat(stParser.currentToken(), equalTo(null)); @@ -300,11 +280,10 @@ private static SignificanceHeuristic parseSignificanceHeuristic( return aggregatorFactory.significanceHeuristic(); } - protected SignificanceHeuristic parseFromString(ParseFieldRegistry significanceHeuristicParserRegistry, - String heuristicString) throws IOException { + protected SignificanceHeuristic parseFromString(String heuristicString) throws IOException { try (XContentParser stParser = createParser(JsonXContent.jsonXContent, "{\"field\":\"text\", " + heuristicString + ", \"min_doc_count\":200}")) { - return parseSignificanceHeuristic(significanceHeuristicParserRegistry, stParser); + return parseSignificanceHeuristic(stParser); } } @@ -494,4 +473,9 @@ public void testGNDCornerCases() throws Exception { gnd = new GND(false); assertThat(gnd.getScore(0, 0, 0, 0), equalTo(0.0)); } + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(new SearchModule(Settings.EMPTY, emptyList()).getNamedXContents()); + } } From 16c25d72d67b0c0531a4e34a5045444dff442721 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Sun, 5 Jan 2020 19:08:11 -0500 Subject: [PATCH 392/686] Adjus bwc for #50502 Relates #50502 --- .../rest-api-spec/test/indices.create/10_basic.yml | 4 ++-- .../rest-api-spec/test/indices.stats/20_translog.yml | 8 ++++---- .../java/org/elasticsearch/test/rest/ESRestTestCase.java | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml index 1dacfd6ad61c3..b5ce95c31a581 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml @@ -123,8 +123,8 @@ --- "Create index without soft deletes": - skip: - version: " - 7.9.99" - reason: "indices without soft deletes are deprecated in 8.0" + version: " - 7.5.99" + reason: "indices without soft deletes are deprecated in 7.6" features: "warnings" - do: diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.stats/20_translog.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.stats/20_translog.yml index 8ba11bb280ce7..0cc21eda1bdcd 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.stats/20_translog.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.stats/20_translog.yml @@ -1,8 +1,8 @@ --- "Translog retention without soft_deletes": - skip: - version: " - 7.9.99" - reason: "indices without soft deletes are deprecated in 8.0" + version: " - 7.5.99" + reason: "indices without soft deletes are deprecated in 7.6" features: "warnings" - do: @@ -138,8 +138,8 @@ --- "Translog stats on closed indices without soft-deletes": - skip: - version: " - 7.9.99" - reason: "indices without soft deletes are deprecated in 8.0" + version: " - 7.5.99" + reason: "indices without soft deletes are deprecated in 7.6" features: "warnings" - do: diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index 4344b54c38eda..b843ea7a3d2b5 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -1008,10 +1008,10 @@ protected static void expectSoftDeletesWarning(Request request, String indexName final List expectedWarnings = List.of( "Creating indices with soft-deletes disabled is deprecated and will be removed in future Elasticsearch versions. " + "Please do not specify value for setting [index.soft_deletes.enabled] of index [" + indexName + "]."); - if (nodeVersions.stream().allMatch(version -> version.onOrAfter(Version.V_8_0_0))) { + if (nodeVersions.stream().allMatch(version -> version.onOrAfter(Version.V_7_6_0))) { request.setOptions(RequestOptions.DEFAULT.toBuilder() .setWarningsHandler(warnings -> warnings.equals(expectedWarnings) == false)); - } else if (nodeVersions.stream().anyMatch(version -> version.onOrAfter(Version.V_8_0_0))) { + } else if (nodeVersions.stream().anyMatch(version -> version.onOrAfter(Version.V_7_6_0))) { request.setOptions(RequestOptions.DEFAULT.toBuilder() .setWarningsHandler(warnings -> warnings.isEmpty() == false && warnings.equals(expectedWarnings) == false)); } From f0c36c6f7371e2fde26cfa345aa1f93cec2b8c7e Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 6 Jan 2020 07:45:49 -0600 Subject: [PATCH 393/686] [DOCS] Warn about using `geo_centroid` as sub-agg to `geohash_grid` (#50038) If `geo_point fields` are multi-valued, using `geo_centroid` as a sub-agg to `geohash_grid` could result in centroids outside of bucket boundaries. This adds a related warning to the geo_centroid agg docs. --- .../metrics/geocentroid-aggregation.asciidoc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/reference/aggregations/metrics/geocentroid-aggregation.asciidoc b/docs/reference/aggregations/metrics/geocentroid-aggregation.asciidoc index d26897c9de7e7..659dec496c87a 100644 --- a/docs/reference/aggregations/metrics/geocentroid-aggregation.asciidoc +++ b/docs/reference/aggregations/metrics/geocentroid-aggregation.asciidoc @@ -143,3 +143,17 @@ The response for the above aggregation: } -------------------------------------------------- // TESTRESPONSE[s/\.\.\./"took": $body.took,"_shards": $body._shards,"hits":$body.hits,"timed_out":false,/] + +[WARNING] +.Using `geo_centroid` as a sub-aggregation of `geohash_grid` +==== +The <> +aggregation places documents, not individual geo-points, into buckets. If a +document's `geo_point` field contains <>, the document +could be assigned to multiple buckets, even if one or more of its geo-points are +outside the bucket boundaries. + +If a `geocentroid` sub-aggregation is also used, each centroid is calculated +using all geo-points in a bucket, including those outside the bucket boundaries. +This can result in centroids outside of bucket boundaries. +==== \ No newline at end of file From 8a4562488d51f0f390cb7ab2a79b1a6246015725 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Mon, 6 Jan 2020 13:58:07 +0000 Subject: [PATCH 394/686] Mute HistogramPercentileAggregationTests.testTDigestHistogram See https://github.com/elastic/elasticsearch/issues/50307 --- .../analytics/mapper/HistogramPercentileAggregationTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramPercentileAggregationTests.java b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramPercentileAggregationTests.java index 1c826449d4a3f..833334660ca65 100644 --- a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramPercentileAggregationTests.java +++ b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramPercentileAggregationTests.java @@ -132,6 +132,7 @@ public void testHDRHistogram() throws Exception { } } + @AwaitsFix( bugUrl = "https://github.com/elastic/elasticsearch/issues/50307") public void testTDigestHistogram() throws Exception { XContentBuilder xContentBuilder = XContentFactory.jsonBuilder() From 8cce9781a70feaa3ab3ecc8f3ec4efe1acf7c0e6 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 6 Jan 2020 14:12:51 +0000 Subject: [PATCH 395/686] Remove the 'local' parameter of /_cat/nodes (#50594) The cat nodes API performs a `ClusterStateAction` then a `NodesInfoAction`. Today it accepts the `?local` parameter and passes this to the `ClusterStateAction` but this parameter has no effect on the `NodesInfoAction`. This is surprising, because `GET _cat/nodes?local` looks like it might be a completely local call but in fact it still depends on every node in the cluster. This parameter was deprecated in 7.x in #50499 and this commit removes it. Relates #50088 --- docs/reference/cat/nodes.asciidoc | 12 ------------ docs/reference/migration/migrate_8_0.asciidoc | 2 ++ .../migration/migrate_8_0/api.asciidoc | 19 +++++++++++++++++++ .../rest-api-spec/api/cat.nodes.json | 8 -------- .../rest/action/cat/RestNodesAction.java | 12 +++--------- .../rest/action/cat/RestNodesActionTests.java | 12 ++++++------ 6 files changed, 30 insertions(+), 35 deletions(-) create mode 100644 docs/reference/migration/migrate_8_0/api.asciidoc diff --git a/docs/reference/cat/nodes.asciidoc b/docs/reference/cat/nodes.asciidoc index 506bc6f5c2721..a5aff2f8daf2d 100644 --- a/docs/reference/cat/nodes.asciidoc +++ b/docs/reference/cat/nodes.asciidoc @@ -285,18 +285,6 @@ Number of suggest operations, such as `0`. include::{docdir}/rest-api/common-parms.asciidoc[tag=help] -include::{docdir}/rest-api/common-parms.asciidoc[tag=local] -+ --- -`local`:: -(Optional, boolean) If `true`, the request computes the list of selected nodes -from the local cluster state. Defaults to `false`, which means the list of -selected nodes is computed from the cluster state on the master node. In either -case the coordinating node sends a request for further information to each -selected node. deprecated::[8.0,This parameter does not cause this API to act -locally. It will be removed in version 8.0.] --- - include::{docdir}/rest-api/common-parms.asciidoc[tag=master-timeout] include::{docdir}/rest-api/common-parms.asciidoc[tag=cat-s] diff --git a/docs/reference/migration/migrate_8_0.asciidoc b/docs/reference/migration/migrate_8_0.asciidoc index eff10899a2e03..9e4532b87ccbe 100644 --- a/docs/reference/migration/migrate_8_0.asciidoc +++ b/docs/reference/migration/migrate_8_0.asciidoc @@ -30,6 +30,7 @@ coming[8.0.0] * <> * <> * <> +* <> //NOTE: The notable-breaking-changes tagged regions are re-used in the //Installation and Upgrade Guide @@ -81,3 +82,4 @@ include::migrate_8_0/reindex.asciidoc[] include::migrate_8_0/search.asciidoc[] include::migrate_8_0/settings.asciidoc[] include::migrate_8_0/indices.asciidoc[] +include::migrate_8_0/api.asciidoc[] diff --git a/docs/reference/migration/migrate_8_0/api.asciidoc b/docs/reference/migration/migrate_8_0/api.asciidoc new file mode 100644 index 0000000000000..f3293a85d5621 --- /dev/null +++ b/docs/reference/migration/migrate_8_0/api.asciidoc @@ -0,0 +1,19 @@ +[float] +[[breaking_80_api_changes]] +=== REST API changes + +//NOTE: The notable-breaking-changes tagged regions are re-used in the +//Installation and Upgrade Guide +//tag::notable-breaking-changes[] + +// end::notable-breaking-changes[] + +[float] +==== Deprecated `?local` parameter removed from `GET _cat/nodes` API + +The `?local` parameter to the `GET _cat/nodes` API was deprecated in 7.x and is +rejected in 8.0. This parameter caused the API to use the local cluster state +to determine the nodes returned by the API rather than the cluster state from +the master, but this API requests information from each selected node +regardless of the `?local` parameter which means this API does not run in a +fully node-local fashion. diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json index 753a6aab1d5f3..20de099e2e9cb 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json @@ -41,14 +41,6 @@ "type":"boolean", "description":"Return the full node ID instead of the shortened version (default: false)" }, - "local":{ - "type":"boolean", - "description":"Calculate the selected nodes using the local cluster state rather than the state from master node (default: false)", - "deprecated":{ - "version":"8.0.0", - "description":"This parameter does not cause this API to act locally." - } - }, "master_timeout":{ "type":"time", "description":"Explicit operation timeout for connection to master node" diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/RestNodesAction.java b/server/src/main/java/org/elasticsearch/rest/action/cat/RestNodesAction.java index 849fbdac49224..ac26c43385004 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/RestNodesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/RestNodesAction.java @@ -19,7 +19,7 @@ package org.elasticsearch.rest.action.cat; -import org.apache.logging.log4j.LogManager; +import org.elasticsearch.Version; import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; @@ -34,7 +34,6 @@ import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.Strings; import org.elasticsearch.common.Table; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.ByteSizeValue; @@ -69,10 +68,6 @@ import static org.elasticsearch.rest.RestRequest.Method.GET; public class RestNodesAction extends AbstractCatAction { - private static final DeprecationLogger deprecationLogger = new DeprecationLogger( - LogManager.getLogger(RestNodesAction.class)); - static final String LOCAL_DEPRECATED_MESSAGE = "Deprecated parameter [local] used. This parameter does not cause this API to act " + - "locally, and should not be used. It will be unsupported in version 8.0."; public RestNodesAction(RestController controller) { controller.registerHandler(GET, "/_cat/nodes", this); @@ -92,10 +87,9 @@ protected void documentation(StringBuilder sb) { public RestChannelConsumer doCatRequest(final RestRequest request, final NodeClient client) { final ClusterStateRequest clusterStateRequest = new ClusterStateRequest(); clusterStateRequest.clear().nodes(true); - if (request.hasParam("local")) { - deprecationLogger.deprecated(LOCAL_DEPRECATED_MESSAGE); + if (request.hasParam("local") && Version.CURRENT.major == Version.V_7_0_0.major + 1) { // only needed in v8 to catch breaking usages + throw new IllegalArgumentException("parameter [local] is not supported"); } - clusterStateRequest.local(request.paramAsBoolean("local", clusterStateRequest.local())); clusterStateRequest.masterNodeTimeout(request.paramAsTime("master_timeout", clusterStateRequest.masterNodeTimeout())); final boolean fullId = request.paramAsBoolean("full_id", false); return channel -> client.admin().cluster().state(clusterStateRequest, new RestActionListener(channel) { diff --git a/server/src/test/java/org/elasticsearch/rest/action/cat/RestNodesActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/cat/RestNodesActionTests.java index 05b14d6b1cfc4..38c4837468362 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/cat/RestNodesActionTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/cat/RestNodesActionTests.java @@ -40,6 +40,7 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; +import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -69,15 +70,14 @@ public void testBuildTableDoesNotThrowGivenNullNodeInfoAndStats() { action.buildTable(false, new FakeRestRequest(), clusterStateResponse, nodesInfoResponse, nodesStatsResponse); } - public void testCatNodesWithLocalDeprecationWarning() { + public void testCatNodesRejectsLocalParameter() { + assumeTrue("test is only needed in v8, can be removed in v9", Version.CURRENT.major == Version.V_7_0_0.major + 1); TestThreadPool threadPool = new TestThreadPool(RestNodesActionTests.class.getName()); NodeClient client = new NodeClient(Settings.EMPTY, threadPool); FakeRestRequest request = new FakeRestRequest(); - request.params().put("local", randomFrom("", "true", "false")); - - action.doCatRequest(request, client); - assertWarnings(RestNodesAction.LOCAL_DEPRECATED_MESSAGE); - + request.params().put("local", randomFrom("", "true", "false", randomAlphaOfLength(10))); + assertThat(expectThrows(IllegalArgumentException.class, () -> action.doCatRequest(request, client)).getMessage(), + is("parameter [local] is not supported")); terminate(threadPool); } } From 71f68c26862878085e3c530b31960026e48d0005 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Mon, 6 Jan 2020 15:37:02 +0100 Subject: [PATCH 396/686] Fix ingest stats test bug. (#50653) This test code fixes a serialization test bug: https://gradle-enterprise.elastic.co/s/7x2ct6yywkw3o Rarely stats for the same processor are generated and the production code then sums up these stats. However the test code wasn't summing up in that case, which caused inconsistencies between the actual and expected results. Closes #50507 --- .../cluster/stats/ClusterStatsNodesTests.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/stats/ClusterStatsNodesTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/stats/ClusterStatsNodesTests.java index 8083d4558c972..9caf1f841478f 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/stats/ClusterStatsNodesTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/stats/ClusterStatsNodesTests.java @@ -70,9 +70,23 @@ public void testNetworkTypesToXContent() throws Exception { public void testIngestStats() throws Exception { NodeStats nodeStats = randomValueOtherThanMany(n -> n.getIngestStats() == null, NodeStatsTests::createNodeStats); SortedMap processorStats = new TreeMap<>(); - nodeStats.getIngestStats().getProcessorStats().values().forEach(l -> l.forEach(s -> processorStats.put(s.getType(), - new long[] { s.getStats().getIngestCount(), s.getStats().getIngestFailedCount(), - s.getStats().getIngestCurrent(), s.getStats().getIngestTimeInMillis()}))); + nodeStats.getIngestStats().getProcessorStats().values().forEach(stats -> { + stats.forEach(stat -> { + processorStats.compute(stat.getType(), (key, value) -> { + if (value == null) { + return new long[] { stat.getStats().getIngestCount(), stat.getStats().getIngestFailedCount(), + stat.getStats().getIngestCurrent(), stat.getStats().getIngestTimeInMillis()}; + } else { + value[0] += stat.getStats().getIngestCount(); + value[1] += stat.getStats().getIngestFailedCount(); + value[2] += stat.getStats().getIngestCurrent(); + value[3] += stat.getStats().getIngestTimeInMillis(); + return value; + } + }); + }); + }); + ClusterStatsNodes.IngestStats stats = new ClusterStatsNodes.IngestStats(Collections.singletonList(nodeStats)); assertThat(stats.pipelineCount, equalTo(nodeStats.getIngestStats().getProcessorStats().size())); String processorStatsString = "{"; From 9cd22c5fa793298466c23d6de1920b07ec5b1f53 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 6 Jan 2020 08:38:21 -0600 Subject: [PATCH 397/686] [DOCS] Remove unneeded redirects (#50510) The docs/reference/redirects.asciidoc file stores a list of relocated or deleted pages for the Elasticsearch Reference documentation. This prunes several older redirects that are no longer needed. --- docs/reference/redirects.asciidoc | 223 ------------------------------ 1 file changed, 223 deletions(-) diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 4c5e26bb2e799..063be6616c9ac 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -26,229 +26,6 @@ index that make warmers not necessary anymore. The types exists endpoint has been removed. See <> for more details. -[role="exclude",id="xpack-api"] -=== X-Pack APIs - -{es} {xpack} APIs are now documented in <>. - -[role="exclude",id="ml-calendar-resource"]] -=== Calendar resources - -See <> and -{ml-docs}/ml-calendars.html[Calendars and scheduled events]. - -[role="exclude",id="ml-filter-resource"] -=== Filter resources - -See <> and -{ml-docs}/ml-rules.html[Machine learning custom rules]. - -[role="exclude",id="ml-event-resource"] -=== Scheduled event resources - -See <> and -{ml-docs}/ml-calendars.html[Calendars and scheduled events]. - -[role="exclude",id="index-apis"] -=== Index APIs -{es} index APIs are now documented in <>. - -[role="exclude",id="search-request-docvalue-fields"] -=== Doc value fields parameter for request body search API -See <>. - -[role="exclude",id="search-request-explain"] -=== Explain parameter for request body search API -See <>. - -[role="exclude",id="search-request-collapse"] -=== Collapse parameter for request body search API -See <>. - -[role="exclude",id="search-request-from-size"] -=== From and size parameters for request body search API -See <>. - -[role="exclude",id="search-request-highlighting"] -=== Highlight parameter for request body search API -See <>. - -[role="exclude",id="search-request-index-boost"] -=== Index boost parameter for request body search API -See <>. - -[role="exclude",id="search-request-inner-hits"] -=== Inner hits parameter for request body search API -See <>. - -[role="exclude",id="search-request-min-score"] -=== Minimum score parameter for request body search API -See <>. - -[role="exclude",id="search-request-named-queries-and-filters"] -=== Named query parameter for request body search API -See <>. - -[role="exclude",id="search-request-post-filter"] -=== Post filter parameter for request body search API -See <>. - -[role="exclude",id="search-request-preference"] -=== Preference parameter for request body search API -See <>. - -[role="exclude",id="search-request-query"] -=== Query parameter for request body search API -See <>. - -[role="exclude",id="search-request-rescore"] -=== Rescoring parameter for request body search API -See <>. - -[role="exclude",id="search-request-script-fields"] -=== Script fields parameter for request body search API -See <>. - -[role="exclude",id="search-request-scroll"] -=== Scroll parameter for request body search API -See <>. - -[role="exclude",id="search-request-search-after"] -=== Search after parameter for request body search API -See <>. - -[role="exclude",id="search-request-search-type"] -=== Search type parameter for request body search API -See <>. - -[role="exclude",id="search-request-seq-no-primary-term"] -=== Sequence numbers and primary terms parameter for request body search API -See <>. - -[role="exclude",id="search-request-sort"] -=== Sort parameter for request body search API -See <>. - -[role="exclude",id="search-request-source-filtering"] -=== Source filtering parameter for request body search API -See <>. - -[role="exclude",id="search-request-stored-fields"] -=== Stored fields parameter for request body search API -See <>. - -[role="exclude",id="search-request-track-total-hits"] -=== Track total hits parameter for request body search API -See <>. - -[role="exclude",id="search-request-version"] -=== Version parameter for request body search API -See <>. - -[role="exclude",id="search-suggesters-term"] -=== Term suggester -See <>. - -[role="exclude",id="search-suggesters-phrase"] -=== Phrase suggester -See <>. - -[role="exclude",id="search-suggesters-completion"] -=== Completion suggester -See <>. - -[role="exclude",id="suggester-context"] -=== Context suggester -See <>. - -[role="exclude",id="returning-suggesters-type"] -=== Return suggester type -See <>. - -[role="exclude",id="search-profile-queries"] -=== Profiling queries -See <>. - -[role="exclude",id="search-profile-aggregations"] -=== Profiling aggregations -See <>. - -[role="exclude",id="search-profile-considerations"] -=== Profiling considerations -See <>. - -[role="exclude",id="_explain_analyze"] -=== Explain analyze API -See <>. - -[role="exclude",id="indices-synced-flush"] -=== Synced flush API -See <>. - -[role="exclude",id="_repositories"] -=== Snapshot repositories -See <>. - -[role="exclude",id="_snapshot"] -=== Snapshot -See <>. - -[role="exclude",id="getting-started-explore"] -=== Exploring your cluster -See <>. - -[role="exclude",id="getting-started-cluster-health"] -=== Cluster health -See <>. - -[role="exclude", id="getting-started-list-indices"] -=== List all indices -See <>. - -[role="exclude", id="getting-started-create-index"] -=== Create an index -See <>. - -[role="exclude", id="getting-started-query-document"] -=== Index and query a document -See <>. - -[role="exclude", id="getting-started-delete-index"] -=== Delete an index -See <>. - -[role="exclude", id="getting-started-modify-data"] -== Modifying your data -See <>. - -[role="exclude", id="indexing-replacing-documents"] -=== Indexing/replacing documents -See <>. - -[role="exclude", id="getting-started-explore-data"] -=== Exploring your data -See <>. - -[role="exclude", id="getting-started-search-API"] -=== Search API -See <>. - -[role="exclude", id="getting-started-conclusion"] -=== Conclusion -See <>. - -[role="exclude",id="ccs-reduction"] -=== {ccs-cap} reduction -See <>. - -[role="exclude",id="administer-elasticsearch"] -=== Administering {es} -See <>. - -[role="exclude",id="slm-api"] -=== Snapshot lifecycle management API -See <>. - [role="exclude",id="delete-data-frame-transform"] === Delete {transforms} API From 63881350a5f9b746e43e44409a5ba70b0c4985eb Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Mon, 6 Jan 2020 11:17:29 -0500 Subject: [PATCH 398/686] Fix testRecoverFromHardDeletesIndex (#50663) We need to create a hard-deletes engine in the test with soft-deletes disabled; otherwise, we the min_retained_seqno will be calculated incorrectly. Closes #50654 --- .../elasticsearch/index/engine/InternalEngineTests.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index c71730b6494f9..2d5dc675f025b 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -5476,10 +5476,14 @@ public long softUpdateDocuments(Term term, Iterable operations = generateHistoryOnReplica(between(1, 500), randomBoolean(), randomBoolean(), randomBoolean()); + final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()) + .settings(Settings.builder().put(defaultSettings.getSettings()).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false)) + .build(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); try (Store store = createStore()) { - EngineConfig config = config(defaultSettings, store, translogPath, NoMergePolicy.INSTANCE, null, null, globalCheckpoint::get); + EngineConfig config = config(indexSettings, store, translogPath, NoMergePolicy.INSTANCE, null, null, globalCheckpoint::get); final List docs; - try (InternalEngine hardDeletesEngine = createEngine(defaultSettings, store, translogPath, newMergePolicy(), + try (InternalEngine hardDeletesEngine = createEngine(indexSettings, store, translogPath, newMergePolicy(), hardDeletesWriter, null, globalCheckpoint::get)) { for (Engine.Operation op : operations) { applyOperation(hardDeletesEngine, op); From 62d0307db1eb9c51536281d62b6334d1e950dda9 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 6 Jan 2020 17:52:20 +0100 Subject: [PATCH 399/686] Fix GCS Mock Broken Handling of some Blobs (#50666) * Fix GCS Mock Broken Handling of some Blobs We were incorrectly handling blobs starting in `\r\n` which broke tests randomly when blobs started on these. Relates #49429 --- .../java/fixture/gcs/GoogleCloudStorageHttpHandler.java | 4 +++- .../blobstore/ESBlobStoreRepositoryIntegTestCase.java | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java index 1b4c6b4caacb9..75cb9a9abe2c7 100644 --- a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java +++ b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java @@ -271,6 +271,7 @@ public byte[] toByteArray() { return buf; } }; + boolean skippedEmptyLine = false; while ((read = in.read()) != -1) { out.reset(); boolean markAndContinue = false; @@ -290,7 +291,7 @@ public byte[] toByteArray() { } while ((read = in.read()) != -1); final String bucketPrefix = "{\"bucket\":"; final String start = new String(out.toByteArray(), 0, Math.min(out.size(), bucketPrefix.length()), UTF_8); - if (start.length() == 0 || start.equals("\r\n") || start.startsWith("--") + if ((skippedEmptyLine == false && start.length() == 0) || start.startsWith("--") || start.toLowerCase(Locale.ROOT).startsWith("content")) { markAndContinue = true; } else if (start.startsWith(bucketPrefix)) { @@ -302,6 +303,7 @@ public byte[] toByteArray() { } } if (markAndContinue) { + skippedEmptyLine = start.length() == 0; in.mark(Integer.MAX_VALUE); continue; } diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java index 62af8e26da073..6ae627972b62c 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java @@ -132,7 +132,11 @@ public void testWriteRead() throws IOException { byte[] buffer = new byte[scaledRandomIntBetween(1, data.length - target.length())]; int offset = scaledRandomIntBetween(0, buffer.length - 1); int read = stream.read(buffer, offset, buffer.length - offset); - target.append(new BytesRef(buffer, offset, read)); + if (read >= 0) { + target.append(new BytesRef(buffer, offset, read)); + } else { + fail("Expected [" + (data.length - target.length()) + "] more bytes to be readable but reached EOF"); + } } assertEquals(data.length, target.length()); assertArrayEquals(data, Arrays.copyOfRange(target.bytes(), 0, target.length())); From b1db9f4cb4c4f621371473997aeff20c8dcff48b Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Mon, 6 Jan 2020 09:58:24 -0700 Subject: [PATCH 400/686] Add aditional logging for ILM history store tests (#50624) These tests use the same index name, making it hard to read logs when diagnosing the failures. Additionally more information about the current state of the index could be retrieved when failing. This changes these two things in the hope of capturing more data about why this fails on some CI nodes but not others. Relates to #50353 Co-authored-by: Elastic Machine --- .../ilm/TimeSeriesLifecycleActionsIT.java | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 16ef7a9639542..47992798cde8e 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -1085,9 +1085,8 @@ public void testRolloverStepRetriesUntilRolledOverIndexIsDeleted() throws Except assertBusy(() -> assertThat(getStepKeyForIndex(index), equalTo(TerminalPolicyStep.KEY))); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50353") public void testHistoryIsWrittenWithSuccess() throws Exception { - String index = "index"; + String index = "success-index"; createNewSingletonPolicy("hot", new RolloverAction(null, null, 1L)); Request createIndexTemplate = new Request("PUT", "_template/rolling_indexes"); @@ -1129,9 +1128,8 @@ public void testHistoryIsWrittenWithSuccess() throws Exception { } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50353") public void testHistoryIsWrittenWithFailure() throws Exception { - String index = "index"; + String index = "failure-index"; createNewSingletonPolicy("hot", new RolloverAction(null, null, 1L)); Request createIndexTemplate = new Request("PUT", "_template/rolling_indexes"); @@ -1158,9 +1156,8 @@ public void testHistoryIsWrittenWithFailure() throws Exception { assertBusy(() -> assertHistoryIsPresent(policy, index + "-1", false, "ERROR"), 30, TimeUnit.SECONDS); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/50353") public void testHistoryIsWrittenWithDeletion() throws Exception { - String index = "index"; + String index = "delete-index"; createNewSingletonPolicy("delete", new DeleteAction()); Request createIndexTemplate = new Request("PUT", "_template/delete_indexes"); @@ -1239,8 +1236,37 @@ private void assertHistoryIsPresent(String policyName, String indexName, boolean historyResponseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); } logger.info("--> history response: {}", historyResponseMap); - assertThat((int)((Map) ((Map) historyResponseMap.get("hits")).get("total")).get("value"), - greaterThanOrEqualTo(1)); + int hits = (int)((Map) ((Map) historyResponseMap.get("hits")).get("total")).get("value"); + + // For a failure, print out whatever history we *do* have for the index + if (hits == 0) { + final Request allResults = new Request("GET", "ilm-history*/_search"); + allResults.setJsonEntity("{\n" + + " \"query\": {\n" + + " \"bool\": {\n" + + " \"must\": [\n" + + " {\n" + + " \"term\": {\n" + + " \"policy\": \"" + policyName + "\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"term\": {\n" + + " \"index\": \"" + indexName + "\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}"); + final Response allResultsResp = client().performRequest(historySearchRequest); + Map allResultsMap; + try (InputStream is = allResultsResp.getEntity().getContent()) { + allResultsMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); + } + logger.info("--> expected at least 1 hit, got 0. All history for index [{}]: {}", index, allResultsMap); + } + assertThat(hits, greaterThanOrEqualTo(1)); } catch (ResponseException e) { // Throw AssertionError instead of an exception if the search fails so that assertBusy works as expected logger.error(e); From e951a8135ac609fbee0c06a515b9849116a64a11 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Mon, 6 Jan 2020 10:02:36 -0700 Subject: [PATCH 401/686] Remove unused IndicesOptions#fromByte method (#50665) This change removes a no longer used method, `fromByte`, in IndicesOptions. This method was necessary for backwards compatibility with versions prior to 6.4.0 and was used when talking to those versions. However, the minimum wire compatibility version has changed and we no longer use this code. --- .../action/support/IndicesOptions.java | 36 ------------------- .../action/support/IndicesOptionsTests.java | 25 ------------- 2 files changed, 61 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/support/IndicesOptions.java b/server/src/main/java/org/elasticsearch/action/support/IndicesOptions.java index 25e7a25335d66..e830ed8795608 100644 --- a/server/src/main/java/org/elasticsearch/action/support/IndicesOptions.java +++ b/server/src/main/java/org/elasticsearch/action/support/IndicesOptions.java @@ -118,42 +118,6 @@ private IndicesOptions(Collection

    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 K3X}ruv3~=TP%M}L diff --git a/docs/reference/transform/images/manage-transforms.jpg b/docs/reference/transform/images/manage-transforms.jpg new file mode 100644 index 0000000000000000000000000000000000000000..86fe7a15512acbcaf748aeccbf2520eec7b2edc8 GIT binary patch literal 196675 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 diff --git a/docs/reference/transform/images/pivot-preview.jpg b/docs/reference/transform/images/pivot-preview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..67b2c79326faad29a9e7d46c965b10064174f48c GIT binary patch literal 140881 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