From 44781e415ee1cedf814b68d448085763edffbb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Wed, 10 Jul 2019 11:51:44 +0200 Subject: [PATCH] [7.x] [ML] Add DatafeedTimingStats to datafeed GetDatafeedStatsAction.Response (#43045) (#44118) --- .../client/ml/datafeed/DatafeedStats.java | 22 ++- .../ml/datafeed/DatafeedTimingStats.java | 122 +++++++++++++++ .../ml/datafeed/DatafeedStatsTests.java | 3 +- .../ml/datafeed/DatafeedTimingStatsTests.java | 92 ++++++++++++ .../ml/apis/datafeedresource.asciidoc | 7 + .../ml/apis/get-datafeed-stats.asciidoc | 7 +- .../ml/action/GetDatafeedsStatsAction.java | 31 +++- .../core/ml/datafeed/DatafeedTimingStats.java | 142 ++++++++++++++++++ .../core/ml/datafeed/DatafeedUpdate.java | 36 +++-- .../persistence/ElasticsearchMappings.java | 17 +++ .../ml/job/results/ReservedFieldNames.java | 4 + .../GetDatafeedStatsActionResponseTests.java | 32 ++-- .../ml/datafeed/DatafeedTimingStatsTests.java | 117 +++++++++++++++ .../ElasticsearchMappingsTests.java | 2 + .../xpack/ml/integration/DatafeedJobsIT.java | 130 +++++++++++++++- .../MlNativeAutodetectIntegTestCase.java | 7 + .../xpack/ml/MachineLearning.java | 19 ++- .../action/TransportDeleteDatafeedAction.java | 25 ++- .../TransportGetDatafeedsStatsAction.java | 59 ++++++-- .../TransportPreviewDatafeedAction.java | 71 +++++---- .../action/TransportStartDatafeedAction.java | 25 ++- .../action/TransportUpdateDatafeedAction.java | 49 ++++-- .../xpack/ml/datafeed/DatafeedJobBuilder.java | 50 +++--- .../datafeed/DatafeedTimingStatsReporter.java | 88 +++++++++++ .../extractor/DataExtractorFactory.java | 14 +- .../AbstractAggregationDataExtractor.java | 15 +- .../aggregation/AggregationDataExtractor.java | 6 +- .../AggregationDataExtractorFactory.java | 14 +- .../aggregation/RollupDataExtractor.java | 7 +- .../RollupDataExtractorFactory.java | 17 ++- .../chunked/ChunkedDataExtractor.java | 25 ++- .../chunked/ChunkedDataExtractorFactory.java | 8 +- .../extractor/scroll/ScrollDataExtractor.java | 14 +- .../scroll/ScrollDataExtractorFactory.java | 36 +++-- .../xpack/ml/job/JobManager.java | 9 +- .../ml/job/persistence/JobDataDeleter.java | 15 ++ .../job/persistence/JobResultsPersister.java | 15 ++ .../job/persistence/JobResultsProvider.java | 98 +++++++++++- .../job/persistence/TimingStatsReporter.java | 6 +- .../ml/datafeed/DatafeedJobBuilderTests.java | 23 ++- .../DatafeedTimingStatsReporterTests.java | 89 +++++++++++ .../extractor/DataExtractorFactoryTests.java | 51 +++++-- .../AggregationDataExtractorFactoryTests.java | 6 +- .../AggregationDataExtractorTests.java | 10 +- .../ChunkedDataExtractorFactoryTests.java | 12 +- .../chunked/ChunkedDataExtractorTests.java | 9 +- .../scroll/ScrollDataExtractorTests.java | 9 +- .../xpack/ml/job/JobManagerTests.java | 16 +- .../job/persistence/JobDataDeleterTests.java | 57 +++++++ .../persistence/JobResultsPersisterTests.java | 37 +++++ .../persistence/JobResultsProviderTests.java | 128 +++++++++++++++- .../persistence/TimingStatsReporterTests.java | 4 +- .../test/ml/get_datafeed_stats.yml | 62 ++++++-- 53 files changed, 1730 insertions(+), 239 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedTimingStats.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedTimingStatsTests.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedTimingStats.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedTimingStatsTests.java create mode 100644 x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedTimingStatsReporter.java create mode 100644 x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedTimingStatsReporterTests.java create mode 100644 x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleterTests.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedStats.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedStats.java index 8a9f9ae9a79dc..42527092558d9 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedStats.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedStats.java @@ -41,9 +41,12 @@ public class DatafeedStats implements ToXContentObject { private final NodeAttributes node; @Nullable private final String assignmentExplanation; + @Nullable + private final DatafeedTimingStats timingStats; public static final ParseField ASSIGNMENT_EXPLANATION = new ParseField("assignment_explanation"); public static final ParseField NODE = new ParseField("node"); + public static final ParseField TIMING_STATS = new ParseField("timing_stats"); public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("datafeed_stats", true, @@ -52,7 +55,8 @@ public class DatafeedStats implements ToXContentObject { DatafeedState datafeedState = DatafeedState.fromString((String)a[1]); NodeAttributes nodeAttributes = (NodeAttributes)a[2]; String assignmentExplanation = (String)a[3]; - return new DatafeedStats(datafeedId, datafeedState, nodeAttributes, assignmentExplanation); + DatafeedTimingStats timingStats = (DatafeedTimingStats)a[4]; + return new DatafeedStats(datafeedId, datafeedState, nodeAttributes, assignmentExplanation, timingStats); } ); static { @@ -60,14 +64,16 @@ public class DatafeedStats implements ToXContentObject { PARSER.declareString(ConstructingObjectParser.constructorArg(), DatafeedState.STATE); PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), NodeAttributes.PARSER, NODE); PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), ASSIGNMENT_EXPLANATION); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), DatafeedTimingStats.PARSER, TIMING_STATS); } public DatafeedStats(String datafeedId, DatafeedState datafeedState, @Nullable NodeAttributes node, - @Nullable String assignmentExplanation) { + @Nullable String assignmentExplanation, @Nullable DatafeedTimingStats timingStats) { this.datafeedId = Objects.requireNonNull(datafeedId); this.datafeedState = Objects.requireNonNull(datafeedState); this.node = node; this.assignmentExplanation = assignmentExplanation; + this.timingStats = timingStats; } public String getDatafeedId() { @@ -86,6 +92,10 @@ public String getAssignmentExplanation() { return assignmentExplanation; } + public DatafeedTimingStats getDatafeedTimingStats() { + return timingStats; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { builder.startObject(); @@ -110,13 +120,16 @@ public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params par if (assignmentExplanation != null) { builder.field(ASSIGNMENT_EXPLANATION.getPreferredName(), assignmentExplanation); } + if (timingStats != null) { + builder.field(TIMING_STATS.getPreferredName(), timingStats); + } builder.endObject(); return builder; } @Override public int hashCode() { - return Objects.hash(datafeedId, datafeedState.toString(), node, assignmentExplanation); + return Objects.hash(datafeedId, datafeedState.toString(), node, assignmentExplanation, timingStats); } @Override @@ -131,6 +144,7 @@ public boolean equals(Object obj) { return Objects.equals(datafeedId, other.datafeedId) && Objects.equals(this.datafeedState, other.datafeedState) && Objects.equals(this.node, other.node) && - Objects.equals(this.assignmentExplanation, other.assignmentExplanation); + Objects.equals(this.assignmentExplanation, other.assignmentExplanation) && + Objects.equals(this.timingStats, other.timingStats); } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedTimingStats.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedTimingStats.java new file mode 100644 index 0000000000000..17617f556cf7b --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedTimingStats.java @@ -0,0 +1,122 @@ +/* + * 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.datafeed; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +public class DatafeedTimingStats implements ToXContentObject { + + public static final ParseField JOB_ID = new ParseField("job_id"); + public static final ParseField SEARCH_COUNT = new ParseField("search_count"); + public static final ParseField TOTAL_SEARCH_TIME_MS = new ParseField("total_search_time_ms"); + + public static final ParseField TYPE = new ParseField("datafeed_timing_stats"); + + public static final ConstructingObjectParser PARSER = createParser(); + + private static ConstructingObjectParser createParser() { + ConstructingObjectParser parser = + new ConstructingObjectParser<>( + "datafeed_timing_stats", + true, + args -> { + String jobId = (String) args[0]; + Long searchCount = (Long) args[1]; + Double totalSearchTimeMs = (Double) args[2]; + return new DatafeedTimingStats(jobId, getOrDefault(searchCount, 0L), getOrDefault(totalSearchTimeMs, 0.0)); + }); + parser.declareString(constructorArg(), JOB_ID); + parser.declareLong(optionalConstructorArg(), SEARCH_COUNT); + parser.declareDouble(optionalConstructorArg(), TOTAL_SEARCH_TIME_MS); + return parser; + } + + private final String jobId; + private long searchCount; + private double totalSearchTimeMs; + + public DatafeedTimingStats(String jobId, long searchCount, double totalSearchTimeMs) { + this.jobId = Objects.requireNonNull(jobId); + this.searchCount = searchCount; + this.totalSearchTimeMs = totalSearchTimeMs; + } + + public String getJobId() { + return jobId; + } + + public long getSearchCount() { + return searchCount; + } + + public double getTotalSearchTimeMs() { + return totalSearchTimeMs; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startObject(); + builder.field(JOB_ID.getPreferredName(), jobId); + builder.field(SEARCH_COUNT.getPreferredName(), searchCount); + builder.field(TOTAL_SEARCH_TIME_MS.getPreferredName(), totalSearchTimeMs); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + DatafeedTimingStats other = (DatafeedTimingStats) obj; + return Objects.equals(this.jobId, other.jobId) + && this.searchCount == other.searchCount + && this.totalSearchTimeMs == other.totalSearchTimeMs; + } + + @Override + public int hashCode() { + return Objects.hash(jobId, searchCount, totalSearchTimeMs); + } + + @Override + public String toString() { + return Strings.toString(this); + } + + private static T getOrDefault(@Nullable T value, T defaultValue) { + return value != null ? value : defaultValue; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedStatsTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedStatsTests.java index 50c0809d201ea..fd2b06a8aa5fd 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedStatsTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedStatsTests.java @@ -50,7 +50,8 @@ public static DatafeedStats createRandomInstance() { attributes); } String assignmentReason = randomBoolean() ? randomAlphaOfLength(10) : null; - return new DatafeedStats(datafeedId, datafeedState, nodeAttributes, assignmentReason); + DatafeedTimingStats timingStats = DatafeedTimingStatsTests.createRandomInstance(); + return new DatafeedStats(datafeedId, datafeedState, nodeAttributes, assignmentReason, timingStats); } @Override diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedTimingStatsTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedTimingStatsTests.java new file mode 100644 index 0000000000000..0a5134606da45 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedTimingStatsTests.java @@ -0,0 +1,92 @@ +/* + * 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.datafeed; + +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; + +public class DatafeedTimingStatsTests extends AbstractXContentTestCase { + + private static final String JOB_ID = "my-job-id"; + + public static DatafeedTimingStats createRandomInstance() { + return new DatafeedTimingStats(randomAlphaOfLength(10), randomLong(), randomDouble()); + } + + @Override + protected DatafeedTimingStats createTestInstance() { + return createRandomInstance(); + } + + @Override + protected DatafeedTimingStats doParseInstance(XContentParser parser) throws IOException { + return DatafeedTimingStats.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + public void testParse_OptionalFieldsAbsent() throws IOException { + String json = "{\"job_id\": \"my-job-id\"}"; + try (XContentParser parser = + XContentFactory.xContent(XContentType.JSON).createParser( + xContentRegistry(), DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) { + DatafeedTimingStats stats = DatafeedTimingStats.PARSER.apply(parser, null); + assertThat(stats.getJobId(), equalTo(JOB_ID)); + assertThat(stats.getSearchCount(), equalTo(0L)); + assertThat(stats.getTotalSearchTimeMs(), equalTo(0.0)); + } + } + + public void testEquals() { + DatafeedTimingStats stats1 = new DatafeedTimingStats(JOB_ID, 5, 100.0); + DatafeedTimingStats stats2 = new DatafeedTimingStats(JOB_ID, 5, 100.0); + DatafeedTimingStats stats3 = new DatafeedTimingStats(JOB_ID, 5, 200.0); + + assertTrue(stats1.equals(stats1)); + assertTrue(stats1.equals(stats2)); + assertFalse(stats2.equals(stats3)); + } + + public void testHashCode() { + DatafeedTimingStats stats1 = new DatafeedTimingStats(JOB_ID, 5, 100.0); + DatafeedTimingStats stats2 = new DatafeedTimingStats(JOB_ID, 5, 100.0); + DatafeedTimingStats stats3 = new DatafeedTimingStats(JOB_ID, 5, 200.0); + + assertEquals(stats1.hashCode(), stats1.hashCode()); + assertEquals(stats1.hashCode(), stats2.hashCode()); + assertNotEquals(stats2.hashCode(), stats3.hashCode()); + } + + public void testConstructorAndGetters() { + DatafeedTimingStats stats = new DatafeedTimingStats(JOB_ID, 5, 123.456); + assertThat(stats.getJobId(), equalTo(JOB_ID)); + assertThat(stats.getSearchCount(), equalTo(5L)); + assertThat(stats.getTotalSearchTimeMs(), equalTo(123.456)); + } +} diff --git a/docs/reference/ml/apis/datafeedresource.asciidoc b/docs/reference/ml/apis/datafeedresource.asciidoc index 5c1e3e74a6ae8..32e1237415d9f 100644 --- a/docs/reference/ml/apis/datafeedresource.asciidoc +++ b/docs/reference/ml/apis/datafeedresource.asciidoc @@ -143,3 +143,10 @@ update their 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/apis/get-datafeed-stats.asciidoc b/docs/reference/ml/apis/get-datafeed-stats.asciidoc index 6ce99785912af..8a71351e76d67 100644 --- a/docs/reference/ml/apis/get-datafeed-stats.asciidoc +++ b/docs/reference/ml/apis/get-datafeed-stats.asciidoc @@ -90,7 +90,12 @@ The API returns the following results: "ml.max_open_jobs": "20" } }, - "assignment_explanation": "" + "assignment_explanation": "", + "timing_stats": { + "job_id": "job-total-requests", + "search_count": 20, + "total_search_time_ms": 120.5 + } } ] } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetDatafeedsStatsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetDatafeedsStatsAction.java index 3ccb49af3faec..5fe1725ff2c1b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetDatafeedsStatsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetDatafeedsStatsAction.java @@ -23,12 +23,15 @@ import org.elasticsearch.xpack.core.action.util.QueryPage; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedState; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import java.io.IOException; import java.util.Map; import java.util.Objects; +import static org.elasticsearch.Version.V_7_4_0; + public class GetDatafeedsStatsAction extends StreamableResponseActionType { public static final GetDatafeedsStatsAction INSTANCE = new GetDatafeedsStatsAction(); @@ -133,13 +136,16 @@ public static class DatafeedStats implements ToXContentObject, Writeable { private DiscoveryNode node; @Nullable private String assignmentExplanation; + @Nullable + private DatafeedTimingStats timingStats; public DatafeedStats(String datafeedId, DatafeedState datafeedState, @Nullable DiscoveryNode node, - @Nullable String assignmentExplanation) { + @Nullable String assignmentExplanation, @Nullable DatafeedTimingStats timingStats) { this.datafeedId = Objects.requireNonNull(datafeedId); this.datafeedState = Objects.requireNonNull(datafeedState); this.node = node; this.assignmentExplanation = assignmentExplanation; + this.timingStats = timingStats; } DatafeedStats(StreamInput in) throws IOException { @@ -147,6 +153,11 @@ public DatafeedStats(String datafeedId, DatafeedState datafeedState, @Nullable D datafeedState = DatafeedState.fromStream(in); node = in.readOptionalWriteable(DiscoveryNode::new); assignmentExplanation = in.readOptionalString(); + if (in.getVersion().onOrAfter(V_7_4_0)) { + timingStats = in.readOptionalWriteable(DatafeedTimingStats::new); + } else { + timingStats = null; + } } public String getDatafeedId() { @@ -165,6 +176,10 @@ public String getAssignmentExplanation() { return assignmentExplanation; } + public DatafeedTimingStats getTimingStats() { + return timingStats; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -189,6 +204,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (assignmentExplanation != null) { builder.field("assignment_explanation", assignmentExplanation); } + if (timingStats != null) { + builder.field("timing_stats", timingStats); + } builder.endObject(); return builder; } @@ -199,11 +217,14 @@ public void writeTo(StreamOutput out) throws IOException { datafeedState.writeTo(out); out.writeOptionalWriteable(node); out.writeOptionalString(assignmentExplanation); + if (out.getVersion().onOrAfter(V_7_4_0)) { + out.writeOptionalWriteable(timingStats); + } } @Override public int hashCode() { - return Objects.hash(datafeedId, datafeedState, node, assignmentExplanation); + return Objects.hash(datafeedId, datafeedState, node, assignmentExplanation, timingStats); } @Override @@ -215,10 +236,11 @@ public boolean equals(Object obj) { return false; } DatafeedStats other = (DatafeedStats) obj; - return Objects.equals(datafeedId, other.datafeedId) && + return Objects.equals(this.datafeedId, other.datafeedId) && Objects.equals(this.datafeedState, other.datafeedState) && Objects.equals(this.node, other.node) && - Objects.equals(this.assignmentExplanation, other.assignmentExplanation); + Objects.equals(this.assignmentExplanation, other.assignmentExplanation) && + Objects.equals(this.timingStats, other.timingStats); } } @@ -237,5 +259,4 @@ protected Reader getReader() { return DatafeedStats::new; } } - } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedTimingStats.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedTimingStats.java new file mode 100644 index 0000000000000..443cb84ecdc0a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedTimingStats.java @@ -0,0 +1,142 @@ +/* + * 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.datafeed; + +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.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +public class DatafeedTimingStats implements ToXContentObject, Writeable { + + public static final ParseField JOB_ID = new ParseField("job_id"); + public static final ParseField SEARCH_COUNT = new ParseField("search_count"); + public static final ParseField TOTAL_SEARCH_TIME_MS = new ParseField("total_search_time_ms"); + + public static final ParseField TYPE = new ParseField("datafeed_timing_stats"); + + public static final ConstructingObjectParser PARSER = createParser(); + + private static ConstructingObjectParser createParser() { + ConstructingObjectParser parser = + new ConstructingObjectParser<>( + "datafeed_timing_stats", + true, + args -> { + String jobId = (String) args[0]; + Long searchCount = (Long) args[1]; + Double totalSearchTimeMs = (Double) args[2]; + return new DatafeedTimingStats(jobId, getOrDefault(searchCount, 0L), getOrDefault(totalSearchTimeMs, 0.0)); + }); + parser.declareString(constructorArg(), JOB_ID); + parser.declareLong(optionalConstructorArg(), SEARCH_COUNT); + parser.declareDouble(optionalConstructorArg(), TOTAL_SEARCH_TIME_MS); + return parser; + } + + public static String documentId(String jobId) { + return jobId + "_datafeed_timing_stats"; + } + + private final String jobId; + private long searchCount; + private double totalSearchTimeMs; + + public DatafeedTimingStats(String jobId, long searchCount, double totalSearchTimeMs) { + this.jobId = Objects.requireNonNull(jobId); + this.searchCount = searchCount; + this.totalSearchTimeMs = totalSearchTimeMs; + } + + public DatafeedTimingStats(String jobId) { + this(jobId, 0, 0); + } + + public DatafeedTimingStats(StreamInput in) throws IOException { + jobId = in.readString(); + searchCount = in.readLong(); + totalSearchTimeMs = in.readDouble(); + } + + public DatafeedTimingStats(DatafeedTimingStats other) { + this(other.jobId, other.searchCount, other.totalSearchTimeMs); + } + + public String getJobId() { + return jobId; + } + + public long getSearchCount() { + return searchCount; + } + + public double getTotalSearchTimeMs() { + return totalSearchTimeMs; + } + + public void incrementTotalSearchTimeMs(double searchTimeMs) { + this.searchCount++; + this.totalSearchTimeMs += searchTimeMs; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(jobId); + out.writeLong(searchCount); + out.writeDouble(totalSearchTimeMs); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startObject(); + builder.field(JOB_ID.getPreferredName(), jobId); + builder.field(SEARCH_COUNT.getPreferredName(), searchCount); + builder.field(TOTAL_SEARCH_TIME_MS.getPreferredName(), totalSearchTimeMs); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + DatafeedTimingStats other = (DatafeedTimingStats) obj; + return Objects.equals(this.jobId, other.jobId) + && this.searchCount == other.searchCount + && this.totalSearchTimeMs == other.totalSearchTimeMs; + } + + @Override + public int hashCode() { + return Objects.hash(jobId, searchCount, totalSearchTimeMs); + } + + @Override + public String toString() { + return Strings.toString(this); + } + + private static T getOrDefault(@Nullable T value, T defaultValue) { + return value != null ? value : defaultValue; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java index 1ae4159cddb9d..d10fa6b80df33 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java @@ -426,57 +426,69 @@ public Builder(DatafeedUpdate config) { this.delayedDataCheckConfig = config.delayedDataCheckConfig; } - public void setId(String datafeedId) { + public Builder setId(String datafeedId) { id = ExceptionsHelper.requireNonNull(datafeedId, DatafeedConfig.ID.getPreferredName()); + return this; } - public void setJobId(String jobId) { + public Builder setJobId(String jobId) { this.jobId = jobId; + return this; } - public void setIndices(List indices) { + public Builder setIndices(List indices) { this.indices = indices; + return this; } - public void setQueryDelay(TimeValue queryDelay) { + public Builder setQueryDelay(TimeValue queryDelay) { this.queryDelay = queryDelay; + return this; } - public void setFrequency(TimeValue frequency) { + public Builder setFrequency(TimeValue frequency) { this.frequency = frequency; + return this; } - public void setQuery(QueryProvider queryProvider) { + public Builder setQuery(QueryProvider queryProvider) { this.queryProvider = queryProvider; + return this; } - private void setAggregationsSafe(AggProvider aggProvider) { + private Builder setAggregationsSafe(AggProvider aggProvider) { if (this.aggProvider != null) { throw ExceptionsHelper.badRequestException("Found two aggregation definitions: [aggs] and [aggregations]"); } setAggregations(aggProvider); + return this; } - public void setAggregations(AggProvider aggProvider) { + public Builder setAggregations(AggProvider aggProvider) { this.aggProvider = aggProvider; + return this; } - public void setScriptFields(List scriptFields) { + public Builder setScriptFields(List scriptFields) { List sorted = new ArrayList<>(scriptFields); sorted.sort(Comparator.comparing(SearchSourceBuilder.ScriptField::fieldName)); this.scriptFields = sorted; + return this; } - public void setDelayedDataCheckConfig(DelayedDataCheckConfig delayedDataCheckConfig) { + public Builder setDelayedDataCheckConfig(DelayedDataCheckConfig delayedDataCheckConfig) { this.delayedDataCheckConfig = delayedDataCheckConfig; + return this; } - public void setScrollSize(int scrollSize) { + public Builder setScrollSize(int scrollSize) { this.scrollSize = scrollSize; + return this; } - public void setChunkingConfig(ChunkingConfig chunkingConfig) { + public Builder setChunkingConfig(ChunkingConfig chunkingConfig) { this.chunkingConfig = chunkingConfig; + return this; } public DatafeedUpdate build() { 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 0fc7770758ad3..77073a23491e6 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 @@ -25,6 +25,7 @@ import org.elasticsearch.plugins.MapperPlugin; import org.elasticsearch.xpack.core.ml.datafeed.ChunkingConfig; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; import org.elasticsearch.xpack.core.ml.datafeed.DelayedDataCheckConfig; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsDest; @@ -510,6 +511,7 @@ public static XContentBuilder resultsMapping(String mappingType, Collection dfStatsMap = XContentHelper.convertToMap(bytes, randomBoolean(), xContentType).v2(); - assertThat(dfStatsMap.size(), is(equalTo(3))); + assertThat(dfStatsMap.size(), is(equalTo(4))); assertThat(dfStatsMap, hasEntry("datafeed_id", "df-id")); assertThat(dfStatsMap, hasEntry("state", "started")); assertThat(dfStatsMap, hasKey("node")); + assertThat(dfStatsMap, hasKey("timing_stats")); Map nodeMap = (Map) dfStatsMap.get("node"); assertThat(nodeMap, hasEntry("id", "df-node-id")); @@ -105,5 +107,11 @@ public void testDatafeedStatsToXContent() throws IOException { assertThat(nodeAttributes.size(), is(equalTo(2))); assertThat(nodeAttributes, hasEntry("ml.enabled", "true")); assertThat(nodeAttributes, hasEntry("ml.max_open_jobs", "5")); + + Map timingStatsMap = (Map) dfStatsMap.get("timing_stats"); + assertThat(timingStatsMap.size(), is(equalTo(3))); + assertThat(timingStatsMap, hasEntry("job_id", "my-job-id")); + assertThat(timingStatsMap, hasEntry("search_count", 5)); + assertThat(timingStatsMap, hasEntry("total_search_time_ms", 123.456)); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedTimingStatsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedTimingStatsTests.java new file mode 100644 index 0000000000000..9ecff4974a751 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedTimingStatsTests.java @@ -0,0 +1,117 @@ +/* + * 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.datafeed; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; + +public class DatafeedTimingStatsTests extends AbstractSerializingTestCase { + + private static final String JOB_ID = "my-job-id"; + + public static DatafeedTimingStats createRandom() { + return new DatafeedTimingStats(randomAlphaOfLength(10), randomLong(), randomDouble()); + } + + @Override + protected DatafeedTimingStats createTestInstance(){ + return createRandom(); + } + + @Override + protected Writeable.Reader instanceReader() { + return DatafeedTimingStats::new; + } + + @Override + protected DatafeedTimingStats doParseInstance(XContentParser parser) { + return DatafeedTimingStats.PARSER.apply(parser, null); + } + + @Override + protected DatafeedTimingStats mutateInstance(DatafeedTimingStats instance) throws IOException { + String jobId = instance.getJobId(); + long searchCount = instance.getSearchCount(); + double totalSearchTimeMs = instance.getTotalSearchTimeMs(); + return new DatafeedTimingStats( + jobId + randomAlphaOfLength(5), + searchCount + 1, + totalSearchTimeMs + randomDoubleBetween(1.0, 100.0, true)); + } + + public void testParse_OptionalFieldsAbsent() throws IOException { + String json = "{\"job_id\": \"my-job-id\"}"; + try (XContentParser parser = + XContentFactory.xContent(XContentType.JSON).createParser( + xContentRegistry(), DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) { + DatafeedTimingStats stats = DatafeedTimingStats.PARSER.apply(parser, null); + assertThat(stats.getJobId(), equalTo(JOB_ID)); + assertThat(stats.getSearchCount(), equalTo(0L)); + assertThat(stats.getTotalSearchTimeMs(), equalTo(0.0)); + } + } + + public void testEquals() { + DatafeedTimingStats stats1 = new DatafeedTimingStats(JOB_ID, 5, 100.0); + DatafeedTimingStats stats2 = new DatafeedTimingStats(JOB_ID, 5, 100.0); + DatafeedTimingStats stats3 = new DatafeedTimingStats(JOB_ID, 5, 200.0); + + assertTrue(stats1.equals(stats1)); + assertTrue(stats1.equals(stats2)); + assertFalse(stats2.equals(stats3)); + } + + public void testHashCode() { + DatafeedTimingStats stats1 = new DatafeedTimingStats(JOB_ID, 5, 100.0); + DatafeedTimingStats stats2 = new DatafeedTimingStats(JOB_ID, 5, 100.0); + DatafeedTimingStats stats3 = new DatafeedTimingStats(JOB_ID, 5, 200.0); + + assertEquals(stats1.hashCode(), stats1.hashCode()); + assertEquals(stats1.hashCode(), stats2.hashCode()); + assertNotEquals(stats2.hashCode(), stats3.hashCode()); + } + + public void testConstructorsAndGetters() { + DatafeedTimingStats stats = new DatafeedTimingStats(JOB_ID, 5, 123.456); + assertThat(stats.getJobId(), equalTo(JOB_ID)); + assertThat(stats.getSearchCount(), equalTo(5L)); + assertThat(stats.getTotalSearchTimeMs(), equalTo(123.456)); + + stats = new DatafeedTimingStats(JOB_ID); + assertThat(stats.getJobId(), equalTo(JOB_ID)); + assertThat(stats.getSearchCount(), equalTo(0L)); + assertThat(stats.getTotalSearchTimeMs(), equalTo(0.0)); + } + + public void testCopyConstructor() { + DatafeedTimingStats stats1 = new DatafeedTimingStats(JOB_ID, 5, 123.456); + DatafeedTimingStats stats2 = new DatafeedTimingStats(stats1); + + assertThat(stats2.getJobId(), equalTo(JOB_ID)); + assertThat(stats2.getSearchCount(), equalTo(5L)); + assertThat(stats2.getTotalSearchTimeMs(), equalTo(123.456)); + } + + public void testIncrementTotalSearchTimeMs() { + DatafeedTimingStats stats = new DatafeedTimingStats(JOB_ID, 5, 100.0); + stats.incrementTotalSearchTimeMs(200.0); + assertThat(stats.getJobId(), equalTo(JOB_ID)); + assertThat(stats.getSearchCount(), equalTo(6L)); + assertThat(stats.getTotalSearchTimeMs(), equalTo(300.0)); + } + + public void testDocumentId() { + assertThat(DatafeedTimingStats.documentId("my-job-id"), equalTo("my-job-id_datafeed_timing_stats")); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappingsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappingsTests.java index b3133ef7f8c17..ee8d921485996 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappingsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappingsTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.config.ModelPlotConfig; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.DataCounts; @@ -82,6 +83,7 @@ public void testResultsMapppingReservedFields() throws Exception { overridden.add(ModelSnapshot.TYPE.getPreferredName()); overridden.add(Quantiles.TYPE.getPreferredName()); overridden.add(TimingStats.TYPE.getPreferredName()); + overridden.add(DatafeedTimingStats.TYPE.getPreferredName()); Set expected = collectResultsDocFieldNames(); expected.removeAll(overridden); diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java index be760909e3ea9..9b6523eb73cc2 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java @@ -21,12 +21,17 @@ import org.elasticsearch.xpack.core.ml.datafeed.ChunkingConfig; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedState; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedUpdate; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.config.JobState; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.DataCounts; +import org.hamcrest.Matcher; import org.junit.After; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -39,6 +44,7 @@ import static org.elasticsearch.xpack.ml.support.BaseMlIntegTestCase.indexDocs; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; public class DatafeedJobsIT extends MlNativeAutodetectIntegTestCase { @@ -49,8 +55,8 @@ public void cleanup() throws Exception { public void testLookbackOnly() throws Exception { client().admin().indices().prepareCreate("data-1") - .addMapping("type", "time", "type=date") - .get(); + .addMapping("type", "time", "type=date") + .get(); long numDocs = randomIntBetween(32, 2048); long now = System.currentTimeMillis(); long oneWeekAgo = now - 604800000; @@ -58,8 +64,8 @@ public void testLookbackOnly() throws Exception { indexDocs(logger, "data-1", numDocs, twoWeeksAgo, oneWeekAgo); client().admin().indices().prepareCreate("data-2") - .addMapping("type", "time", "type=date") - .get(); + .addMapping("type", "time", "type=date") + .get(); client().admin().cluster().prepareHealth("data-1", "data-2").setWaitForYellowStatus().get(); long numDocs2 = randomIntBetween(32, 2048); indexDocs(logger, "data-2", numDocs2, oneWeekAgo, now); @@ -92,6 +98,122 @@ public void testLookbackOnly() throws Exception { waitUntilJobIsClosed(job.getId()); } + public void testDatafeedTimingStats_DatafeedRecreated() throws Exception { + client().admin().indices().prepareCreate("data") + .addMapping("type", "time", "type=date") + .get(); + long numDocs = randomIntBetween(32, 2048); + Instant now = Instant.now(); + indexDocs(logger, "data", numDocs, now.minus(Duration.ofDays(14)).toEpochMilli(), now.toEpochMilli()); + + Job.Builder job = createScheduledJob("lookback-job"); + + String datafeedId = "lookback-datafeed"; + DatafeedConfig datafeedConfig = createDatafeed(datafeedId, job.getId(), Arrays.asList("data")); + + registerJob(job); + putJob(job); + + for (int i = 0; i < 2; ++i) { + openJob(job.getId()); + assertBusy(() -> assertEquals(getJobStats(job.getId()).get(0).getState(), JobState.OPENED)); + registerDatafeed(datafeedConfig); + putDatafeed(datafeedConfig); + // Datafeed did not do anything yet, hence search_count is equal to 0. + assertDatafeedStats(datafeedId, DatafeedState.STOPPED, job.getId(), equalTo(0L)); + startDatafeed(datafeedId, 0L, now.toEpochMilli()); + assertBusy(() -> { + assertThat(getDataCounts(job.getId()).getProcessedRecordCount(), equalTo(numDocs)); + // Datafeed processed numDocs documents so search_count must be greater than 0. + assertDatafeedStats(datafeedId, DatafeedState.STOPPED, job.getId(), greaterThan(0L)); + }, 60, TimeUnit.SECONDS); + deleteDatafeed(datafeedId); + waitUntilJobIsClosed(job.getId()); + } + } + + public void testDatafeedTimingStats_DatafeedJobIdUpdated() throws Exception { + client().admin().indices().prepareCreate("data") + .addMapping("type", "time", "type=date") + .get(); + long numDocs = randomIntBetween(32, 2048); + Instant now = Instant.now(); + indexDocs(logger, "data", numDocs, now.minus(Duration.ofDays(14)).toEpochMilli(), now.toEpochMilli()); + + Job.Builder jobA = createScheduledJob("lookback-job"); + Job.Builder jobB = createScheduledJob("other-lookback-job"); + for (Job.Builder job : Arrays.asList(jobA, jobB)) { + registerJob(job); + putJob(job); + } + + String datafeedId = "lookback-datafeed"; + DatafeedConfig datafeedConfig = createDatafeed(datafeedId, jobA.getId(), Arrays.asList("data")); + registerDatafeed(datafeedConfig); + putDatafeed(datafeedConfig); + + for (Job.Builder job : Arrays.asList(jobA, jobB, jobA)) { + openJob(job.getId()); + assertBusy(() -> assertEquals(getJobStats(job.getId()).get(0).getState(), JobState.OPENED)); + // Bind datafeedId to the current job on the list, timing stats are wiped out. + updateDatafeed(new DatafeedUpdate.Builder(datafeedId).setJobId(job.getId()).build()); + // Datafeed did not do anything yet, hence search_count is equal to 0. + assertDatafeedStats(datafeedId, DatafeedState.STOPPED, job.getId(), equalTo(0L)); + startDatafeed(datafeedId, 0L, now.toEpochMilli()); + assertBusy(() -> { + assertThat(getDataCounts(job.getId()).getProcessedRecordCount(), equalTo(numDocs)); + // Datafeed processed numDocs documents so search_count must be greater than 0. + assertDatafeedStats(datafeedId, DatafeedState.STOPPED, job.getId(), greaterThan(0L)); + }, 60, TimeUnit.SECONDS); + waitUntilJobIsClosed(job.getId()); + } + } + + public void testDatafeedTimingStats_QueryDelayUpdated_TimingStatsNotReset() throws Exception { + client().admin().indices().prepareCreate("data") + .addMapping("type", "time", "type=date") + .get(); + long numDocs = randomIntBetween(32, 2048); + Instant now = Instant.now(); + indexDocs(logger, "data", numDocs, now.minus(Duration.ofDays(14)).toEpochMilli(), now.toEpochMilli()); + + Job.Builder job = createScheduledJob("lookback-job"); + registerJob(job); + putJob(job); + + String datafeedId = "lookback-datafeed"; + DatafeedConfig datafeedConfig = createDatafeed(datafeedId, job.getId(), Arrays.asList("data")); + registerDatafeed(datafeedConfig); + putDatafeed(datafeedConfig); + + openJob(job.getId()); + assertBusy(() -> assertEquals(getJobStats(job.getId()).get(0).getState(), JobState.OPENED)); + // Datafeed did not do anything yet, hence search_count is equal to 0. + assertDatafeedStats(datafeedId, DatafeedState.STOPPED, job.getId(), equalTo(0L)); + startDatafeed(datafeedId, 0L, now.toEpochMilli()); + assertBusy(() -> { + assertThat(getDataCounts(job.getId()).getProcessedRecordCount(), equalTo(numDocs)); + // Datafeed processed numDocs documents so search_count must be greater than 0. + assertDatafeedStats(datafeedId, DatafeedState.STOPPED, job.getId(), greaterThan(0L)); + }, 60, TimeUnit.SECONDS); + waitUntilJobIsClosed(job.getId()); + + // Change something different than jobId, here: queryDelay. + updateDatafeed(new DatafeedUpdate.Builder(datafeedId).setQueryDelay(TimeValue.timeValueSeconds(777)).build()); + // Search_count is still greater than 0 (i.e. has not been reset by datafeed update) + assertDatafeedStats(datafeedId, DatafeedState.STOPPED, job.getId(), greaterThan(0L)); + } + + private void assertDatafeedStats(String datafeedId, DatafeedState state, String jobId, Matcher searchCountMatcher) { + GetDatafeedsStatsAction.Request request = new GetDatafeedsStatsAction.Request(datafeedId); + GetDatafeedsStatsAction.Response response = client().execute(GetDatafeedsStatsAction.INSTANCE, request).actionGet(); + assertThat(response.getResponse().results(), hasSize(1)); + GetDatafeedsStatsAction.Response.DatafeedStats stats = response.getResponse().results().get(0); + assertThat(stats.getDatafeedState(), equalTo(state)); + assertThat(stats.getTimingStats().getJobId(), equalTo(jobId)); + assertThat(stats.getTimingStats().getSearchCount(), searchCountMatcher); + } + public void testRealtime() throws Exception { String jobId = "realtime-job"; String datafeedId = jobId + "-datafeed"; 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 9e3c6de561249..fad76f8b23f80 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 @@ -43,11 +43,13 @@ import org.elasticsearch.xpack.core.ml.action.RevertModelSnapshotAction; import org.elasticsearch.xpack.core.ml.action.StartDatafeedAction; 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; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedUpdate; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.config.JobState; import org.elasticsearch.xpack.core.ml.job.config.JobUpdate; @@ -175,6 +177,11 @@ protected StopDatafeedAction.Response stopDatafeed(String datafeedId) { return client().execute(StopDatafeedAction.INSTANCE, request).actionGet(); } + protected PutDatafeedAction.Response updateDatafeed(DatafeedUpdate update) { + UpdateDatafeedAction.Request request = new UpdateDatafeedAction.Request(update); + return client().execute(UpdateDatafeedAction.INSTANCE, request).actionGet(); + } + protected AcknowledgedResponse deleteDatafeed(String datafeedId) { DeleteDatafeedAction.Request request = new DeleteDatafeedAction.Request(datafeedId); return client().execute(DeleteDatafeedAction.INSTANCE, request).actionGet(); 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 cdb2b07ae8f32..52f300bd7d817 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 @@ -447,12 +447,15 @@ public Collection createComponents(Client client, ClusterService cluster Auditor auditor = new Auditor(client, clusterService.getNodeName()); JobResultsProvider jobResultsProvider = new JobResultsProvider(client, settings); + JobResultsPersister jobResultsPersister = new JobResultsPersister(client); + JobDataCountsPersister jobDataCountsPersister = new JobDataCountsPersister(client); JobConfigProvider jobConfigProvider = new JobConfigProvider(client, xContentRegistry); DatafeedConfigProvider datafeedConfigProvider = new DatafeedConfigProvider(client, xContentRegistry); UpdateJobProcessNotifier notifier = new UpdateJobProcessNotifier(client, clusterService, threadPool); JobManager jobManager = new JobManager(env, settings, jobResultsProvider, + jobResultsPersister, clusterService, auditor, threadPool, @@ -463,9 +466,6 @@ public Collection createComponents(Client client, ClusterService cluster // special holder for @link(MachineLearningFeatureSetUsage) which needs access to job manager if ML is enabled JobManagerHolder jobManagerHolder = new JobManagerHolder(jobManager); - JobDataCountsPersister jobDataCountsPersister = new JobDataCountsPersister(client); - JobResultsPersister jobResultsPersister = new JobResultsPersister(client); - NativeStorageProvider nativeStorageProvider = new NativeStorageProvider(environment, MIN_DISK_SPACE_OFF_HEAP.get(settings)); AutodetectProcessFactory autodetectProcessFactory; @@ -509,8 +509,16 @@ public Collection createComponents(Client client, ClusterService cluster xContentRegistry, auditor, clusterService, jobManager, jobResultsProvider, jobResultsPersister, jobDataCountsPersister, autodetectProcessFactory, normalizerFactory, nativeStorageProvider); this.autodetectProcessManager.set(autodetectProcessManager); - DatafeedJobBuilder datafeedJobBuilder = new DatafeedJobBuilder(client, settings, xContentRegistry, - auditor, System::currentTimeMillis); + DatafeedJobBuilder datafeedJobBuilder = + new DatafeedJobBuilder( + client, + xContentRegistry, + auditor, + System::currentTimeMillis, + jobConfigProvider, + jobResultsProvider, + datafeedConfigProvider, + jobResultsPersister); DatafeedManager datafeedManager = new DatafeedManager(threadPool, client, clusterService, datafeedJobBuilder, System::currentTimeMillis, auditor, autodetectProcessManager); this.datafeedManager.set(datafeedManager); @@ -541,6 +549,7 @@ public Collection createComponents(Client client, ClusterService cluster return Arrays.asList( mlLifeCycleService, jobResultsProvider, + jobResultsPersister, jobConfigProvider, datafeedConfigProvider, jobManager, diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java index 1a6bbfb8f2a3b..5426ac7405b66 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java @@ -32,6 +32,7 @@ import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import org.elasticsearch.xpack.ml.MlConfigMigrationEligibilityCheck; import org.elasticsearch.xpack.ml.datafeed.persistence.DatafeedConfigProvider; +import org.elasticsearch.xpack.ml.job.persistence.JobDataDeleter; import java.io.IOException; @@ -143,10 +144,26 @@ private void deleteDatafeedConfig(DeleteDatafeedAction.Request request, ActionLi return; } - datafeedConfigProvider.deleteDatafeedConfig(request.getDatafeedId(), ActionListener.wrap( - deleteResponse -> listener.onResponse(new AcknowledgedResponse(true)), - listener::onFailure - )); + String datafeedId = request.getDatafeedId(); + + datafeedConfigProvider.getDatafeedConfig( + datafeedId, + ActionListener.wrap( + datafeedConfigBuilder -> { + String jobId = datafeedConfigBuilder.build().getJobId(); + JobDataDeleter jobDataDeleter = new JobDataDeleter(client, jobId); + jobDataDeleter.deleteDatafeedTimingStats( + ActionListener.wrap( + unused1 -> { + datafeedConfigProvider.deleteDatafeedConfig( + datafeedId, + ActionListener.wrap( + unused2 -> listener.onResponse(new AcknowledgedResponse(true)), + listener::onFailure)); + }, + listener::onFailure)); + }, + listener::onFailure)); } @Override diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDatafeedsStatsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDatafeedsStatsAction.java index 6b32d414516a0..5124f52d8e5f9 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDatafeedsStatsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetDatafeedsStatsAction.java @@ -23,7 +23,9 @@ import org.elasticsearch.xpack.core.action.util.QueryPage; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedState; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; import org.elasticsearch.xpack.ml.datafeed.persistence.DatafeedConfigProvider; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsProvider; import java.util.List; import java.util.stream.Collectors; @@ -32,15 +34,17 @@ public class TransportGetDatafeedsStatsAction extends TransportMasterNodeReadAct GetDatafeedsStatsAction.Response> { private final DatafeedConfigProvider datafeedConfigProvider; + private final JobResultsProvider jobResultsProvider; @Inject public TransportGetDatafeedsStatsAction(TransportService transportService, ClusterService clusterService, ThreadPool threadPool, ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, - DatafeedConfigProvider datafeedConfigProvider) { + DatafeedConfigProvider datafeedConfigProvider, JobResultsProvider jobResultsProvider) { super(GetDatafeedsStatsAction.NAME, transportService, clusterService, threadPool, actionFilters, GetDatafeedsStatsAction.Request::new, indexNameExpressionResolver); this.datafeedConfigProvider = datafeedConfigProvider; + this.jobResultsProvider = jobResultsProvider; } @Override @@ -58,22 +62,46 @@ protected void masterOperation(GetDatafeedsStatsAction.Request request, ClusterS ActionListener listener) throws Exception { logger.debug("Get stats for datafeed '{}'", request.getDatafeedId()); - datafeedConfigProvider.expandDatafeedIds(request.getDatafeedId(), request.allowNoDatafeeds(), ActionListener.wrap( - expandedDatafeedIds -> { - PersistentTasksCustomMetaData tasksInProgress = state.getMetaData().custom(PersistentTasksCustomMetaData.TYPE); - List results = expandedDatafeedIds.stream() - .map(datafeedId -> getDatafeedStats(datafeedId, state, tasksInProgress)) + datafeedConfigProvider.expandDatafeedConfigs( + request.getDatafeedId(), + request.allowNoDatafeeds(), + ActionListener.wrap( + datafeedBuilders -> { + List jobIds = + datafeedBuilders.stream() + .map(DatafeedConfig.Builder::build) + .map(DatafeedConfig::getJobId) .collect(Collectors.toList()); - QueryPage statsPage = new QueryPage<>(results, results.size(), - DatafeedConfig.RESULTS_FIELD); - listener.onResponse(new GetDatafeedsStatsAction.Response(statsPage)); + jobResultsProvider.datafeedTimingStats( + jobIds, + timingStatsByJobId -> { + PersistentTasksCustomMetaData tasksInProgress = state.getMetaData().custom(PersistentTasksCustomMetaData.TYPE); + List results = + datafeedBuilders.stream() + .map(DatafeedConfig.Builder::build) + .map( + datafeed -> getDatafeedStats( + datafeed.getId(), + state, + tasksInProgress, + datafeed.getJobId(), + timingStatsByJobId.get(datafeed.getJobId()))) + .collect(Collectors.toList()); + QueryPage statsPage = + new QueryPage<>(results, results.size(), DatafeedConfig.RESULTS_FIELD); + listener.onResponse(new GetDatafeedsStatsAction.Response(statsPage)); + }, + listener::onFailure); }, - listener::onFailure - )); + listener::onFailure) + ); } - private static GetDatafeedsStatsAction.Response.DatafeedStats getDatafeedStats(String datafeedId, ClusterState state, - PersistentTasksCustomMetaData tasks) { + private static GetDatafeedsStatsAction.Response.DatafeedStats getDatafeedStats(String datafeedId, + ClusterState state, + PersistentTasksCustomMetaData tasks, + String jobId, + DatafeedTimingStats timingStats) { PersistentTasksCustomMetaData.PersistentTask task = MlTasks.getDatafeedTask(datafeedId, tasks); DatafeedState datafeedState = MlTasks.getDatafeedState(datafeedId, tasks); DiscoveryNode node = null; @@ -82,7 +110,10 @@ private static GetDatafeedsStatsAction.Response.DatafeedStats getDatafeedStats(S node = state.nodes().get(task.getExecutorNode()); explanation = task.getAssignment().getExplanation(); } - return new GetDatafeedsStatsAction.Response.DatafeedStats(datafeedId, datafeedState, node, explanation); + if (timingStats == null) { + timingStats = new DatafeedTimingStats(jobId); + } + return new GetDatafeedsStatsAction.Response.DatafeedStats(datafeedId, datafeedState, node, explanation, timingStats); } @Override diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPreviewDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPreviewDatafeedAction.java index 89ad54e9c1802..d2590cf4cd9bd 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPreviewDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPreviewDatafeedAction.java @@ -20,9 +20,12 @@ import org.elasticsearch.xpack.core.ml.datafeed.ChunkingConfig; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.datafeed.extractor.DataExtractor; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; import org.elasticsearch.xpack.ml.datafeed.persistence.DatafeedConfigProvider; import org.elasticsearch.xpack.ml.job.persistence.JobConfigProvider; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsProvider; import java.io.BufferedReader; import java.io.InputStream; @@ -39,56 +42,68 @@ public class TransportPreviewDatafeedAction extends HandledTransportAction) PreviewDatafeedAction.Request::new); this.threadPool = threadPool; this.client = client; this.jobConfigProvider = jobConfigProvider; this.datafeedConfigProvider = datafeedConfigProvider; + this.jobResultsProvider = jobResultsProvider; + this.jobResultsPersister = jobResultsPersister; this.xContentRegistry = xContentRegistry; } @Override protected void doExecute(Task task, PreviewDatafeedAction.Request request, ActionListener listener) { - datafeedConfigProvider.getDatafeedConfig(request.getDatafeedId(), ActionListener.wrap( - datafeedConfigBuilder -> { - DatafeedConfig datafeedConfig = datafeedConfigBuilder.build(); - jobConfigProvider.getJob(datafeedConfig.getJobId(), ActionListener.wrap( - jobBuilder -> { - DatafeedConfig.Builder previewDatafeed = buildPreviewDatafeed(datafeedConfig); - Map headers = threadPool.getThreadContext().getHeaders().entrySet().stream() - .filter(e -> ClientHelper.SECURITY_HEADER_FILTERS.contains(e.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - previewDatafeed.setHeaders(headers); + datafeedConfigBuilder -> { + DatafeedConfig datafeedConfig = datafeedConfigBuilder.build(); + jobConfigProvider.getJob(datafeedConfig.getJobId(), ActionListener.wrap( + jobBuilder -> { + DatafeedConfig.Builder previewDatafeed = buildPreviewDatafeed(datafeedConfig); + Map headers = threadPool.getThreadContext().getHeaders().entrySet().stream() + .filter(e -> ClientHelper.SECURITY_HEADER_FILTERS.contains(e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + previewDatafeed.setHeaders(headers); + jobResultsProvider.datafeedTimingStats( + jobBuilder.getId(), + timingStats -> { // NB: this is using the client from the transport layer, NOT the internal client. // This is important because it means the datafeed search will fail if the user // requesting the preview doesn't have permission to search the relevant indices. - DataExtractorFactory.create(client, previewDatafeed.build(), jobBuilder.build(), xContentRegistry, - new ActionListener() { - @Override - public void onResponse(DataExtractorFactory dataExtractorFactory) { - DataExtractor dataExtractor = dataExtractorFactory.newExtractor(0, Long.MAX_VALUE); - threadPool.generic().execute(() -> previewDatafeed(dataExtractor, listener)); - } + DataExtractorFactory.create( + client, + previewDatafeed.build(), + jobBuilder.build(), + xContentRegistry, + new DatafeedTimingStatsReporter(timingStats, jobResultsPersister), + new ActionListener() { + @Override + public void onResponse(DataExtractorFactory dataExtractorFactory) { + DataExtractor dataExtractor = dataExtractorFactory.newExtractor(0, Long.MAX_VALUE); + threadPool.generic().execute(() -> previewDatafeed(dataExtractor, listener)); + } - @Override - public void onFailure(Exception e) { - listener.onFailure(e); - } - }); + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); }, - listener::onFailure - )); - }, - listener::onFailure - )); + listener::onFailure); + }, + listener::onFailure)); + }, + listener::onFailure)); } /** Visible for testing */ diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java index 2da7b3291c7e1..a95aba7edff95 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java @@ -43,6 +43,7 @@ import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedJobValidator; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedState; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.config.JobState; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; @@ -50,9 +51,11 @@ import org.elasticsearch.xpack.ml.MlConfigMigrationEligibilityCheck; import org.elasticsearch.xpack.ml.datafeed.DatafeedManager; import org.elasticsearch.xpack.ml.datafeed.DatafeedNodeSelector; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; import org.elasticsearch.xpack.ml.datafeed.persistence.DatafeedConfigProvider; import org.elasticsearch.xpack.ml.job.persistence.JobConfigProvider; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; import org.elasticsearch.xpack.ml.notifications.Auditor; import java.io.IOException; @@ -79,6 +82,7 @@ public class TransportStartDatafeedAction extends TransportMasterNodeAction> listener) { - DataExtractorFactory.create(client, datafeed, job, xContentRegistry, ActionListener.wrap( - dataExtractorFactory -> - persistentTasksService.sendStartRequest(MlTasks.datafeedTaskId(params.getDatafeedId()), - MlTasks.DATAFEED_TASK_NAME, params, listener) - , listener::onFailure)); + DataExtractorFactory.create( + client, + datafeed, + job, + xContentRegistry, + // Creating fake {@link TimingStatsReporter} so that search API call is not needed. + new DatafeedTimingStatsReporter(new DatafeedTimingStats(job.getId()), jobResultsPersister), + ActionListener.wrap( + unused -> + persistentTasksService.sendStartRequest( + MlTasks.datafeedTaskId(params.getDatafeedId()), MlTasks.DATAFEED_TASK_NAME, params, listener), + listener::onFailure)); } @Override diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpdateDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpdateDatafeedAction.java index b7029eff79cd8..caea753acce89 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpdateDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpdateDatafeedAction.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.index.reindex.BulkByScrollResponse; import org.elasticsearch.persistent.PersistentTasksCustomMetaData; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -30,12 +31,14 @@ import org.elasticsearch.xpack.ml.MlConfigMigrationEligibilityCheck; import org.elasticsearch.xpack.ml.datafeed.persistence.DatafeedConfigProvider; import org.elasticsearch.xpack.ml.job.persistence.JobConfigProvider; +import org.elasticsearch.xpack.ml.job.persistence.JobDataDeleter; import java.util.Collections; import java.util.Map; public class TransportUpdateDatafeedAction extends TransportMasterNodeAction { + private final Client client; private final DatafeedConfigProvider datafeedConfigProvider; private final JobConfigProvider jobConfigProvider; private final MlConfigMigrationEligibilityCheck migrationEligibilityCheck; @@ -48,9 +51,10 @@ public TransportUpdateDatafeedAction(Settings settings, TransportService transpo super(UpdateDatafeedAction.NAME, transportService, clusterService, threadPool, actionFilters, indexNameExpressionResolver, UpdateDatafeedAction.Request::new); - datafeedConfigProvider = new DatafeedConfigProvider(client, xContentRegistry); - jobConfigProvider = new JobConfigProvider(client, xContentRegistry); - migrationEligibilityCheck = new MlConfigMigrationEligibilityCheck(settings, clusterService); + this.client = client; + this.datafeedConfigProvider = new DatafeedConfigProvider(client, xContentRegistry); + this.jobConfigProvider = new JobConfigProvider(client, xContentRegistry); + this.migrationEligibilityCheck = new MlConfigMigrationEligibilityCheck(settings, clusterService); } @Override @@ -85,21 +89,42 @@ protected void masterOperation(UpdateDatafeedAction.Request request, ClusterStat String datafeedId = request.getUpdate().getId(); - CheckedConsumer updateConsumer = ok -> { - datafeedConfigProvider.updateDatefeedConfig(request.getUpdate().getId(), request.getUpdate(), headers, + CheckedConsumer updateConsumer = + unused -> { + datafeedConfigProvider.updateDatefeedConfig( + datafeedId, + request.getUpdate(), + headers, jobConfigProvider::validateDatafeedJob, ActionListener.wrap( - updatedConfig -> listener.onResponse(new PutDatafeedAction.Response(updatedConfig)), - listener::onFailure - )); - }; + updatedConfig -> listener.onResponse(new PutDatafeedAction.Response(updatedConfig)), + listener::onFailure)); + }; + + CheckedConsumer deleteTimingStatsAndUpdateConsumer = + unused -> { + datafeedConfigProvider.getDatafeedConfig( + datafeedId, + ActionListener.wrap( + datafeedConfigBuilder -> { + String jobId = datafeedConfigBuilder.build().getJobId(); + if (jobId.equals(request.getUpdate().getJobId())) { + // Datafeed's jobId didn't change, no point in deleting datafeed timing stats. + updateConsumer.accept(null); + } else { + JobDataDeleter jobDataDeleter = new JobDataDeleter(client, jobId); + jobDataDeleter.deleteDatafeedTimingStats(ActionListener.wrap(updateConsumer, listener::onFailure)); + } + }, + listener::onFailure)); + }; if (request.getUpdate().getJobId() != null) { - checkJobDoesNotHaveADifferentDatafeed(request.getUpdate().getJobId(), datafeedId, - ActionListener.wrap(updateConsumer, listener::onFailure)); + checkJobDoesNotHaveADifferentDatafeed( + request.getUpdate().getJobId(), datafeedId, ActionListener.wrap(deleteTimingStatsAndUpdateConsumer, listener::onFailure)); } else { - updateConsumer.accept(Boolean.TRUE); + updateConsumer.accept(null); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobBuilder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobBuilder.java index db862286ca987..0689b9774b56a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobBuilder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobBuilder.java @@ -8,12 +8,12 @@ import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.client.Client; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.xpack.core.action.util.QueryPage; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedJobValidator; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; import org.elasticsearch.xpack.core.ml.job.config.DataDescription; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.DataCounts; @@ -25,6 +25,7 @@ import org.elasticsearch.xpack.ml.datafeed.persistence.DatafeedConfigProvider; import org.elasticsearch.xpack.ml.job.persistence.BucketsQueryBuilder; import org.elasticsearch.xpack.ml.job.persistence.JobConfigProvider; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; import org.elasticsearch.xpack.ml.job.persistence.JobResultsProvider; import org.elasticsearch.xpack.ml.notifications.Auditor; @@ -37,36 +38,28 @@ public class DatafeedJobBuilder { private final Client client; - private final Settings settings; private final NamedXContentRegistry xContentRegistry; private final Auditor auditor; private final Supplier currentTimeSupplier; - - public DatafeedJobBuilder(Client client, Settings settings, NamedXContentRegistry xContentRegistry, - Auditor auditor, Supplier currentTimeSupplier) { + private final JobConfigProvider jobConfigProvider; + private final JobResultsProvider jobResultsProvider; + private final DatafeedConfigProvider datafeedConfigProvider; + private final JobResultsPersister jobResultsPersister; + + public DatafeedJobBuilder(Client client, NamedXContentRegistry xContentRegistry, Auditor auditor, Supplier currentTimeSupplier, + JobConfigProvider jobConfigProvider, JobResultsProvider jobResultsProvider, + DatafeedConfigProvider datafeedConfigProvider, JobResultsPersister jobResultsPersister) { this.client = client; - this.settings = Objects.requireNonNull(settings); this.xContentRegistry = Objects.requireNonNull(xContentRegistry); this.auditor = Objects.requireNonNull(auditor); this.currentTimeSupplier = Objects.requireNonNull(currentTimeSupplier); + this.jobConfigProvider = Objects.requireNonNull(jobConfigProvider); + this.jobResultsProvider = Objects.requireNonNull(jobResultsProvider); + this.datafeedConfigProvider = Objects.requireNonNull(datafeedConfigProvider); + this.jobResultsPersister = Objects.requireNonNull(jobResultsPersister); } void build(String datafeedId, ActionListener listener) { - - JobResultsProvider jobResultsProvider = new JobResultsProvider(client, settings); - JobConfigProvider jobConfigProvider = new JobConfigProvider(client, xContentRegistry); - DatafeedConfigProvider datafeedConfigProvider = new DatafeedConfigProvider(client, xContentRegistry); - - build(datafeedId, jobResultsProvider, jobConfigProvider, datafeedConfigProvider, listener); - } - - /** - * For testing only. - * Use {@link #build(String, ActionListener)} instead - */ - void build(String datafeedId, JobResultsProvider jobResultsProvider, JobConfigProvider jobConfigProvider, - DatafeedConfigProvider datafeedConfigProvider, ActionListener listener) { - AtomicReference jobHolder = new AtomicReference<>(); AtomicReference datafeedConfigHolder = new AtomicReference<>(); @@ -98,11 +91,21 @@ void build(String datafeedId, JobResultsProvider jobResultsProvider, JobConfigPr ); // Create data extractor factory + Consumer datafeedTimingStatsHandler = timingStats -> { + DataExtractorFactory.create( + client, + datafeedConfigHolder.get(), + jobHolder.get(), + xContentRegistry, + new DatafeedTimingStatsReporter(timingStats, jobResultsPersister), + dataExtractorFactoryHandler); + }; + Consumer dataCountsHandler = dataCounts -> { if (dataCounts.getLatestRecordTimeStamp() != null) { context.latestRecordTimeMs = dataCounts.getLatestRecordTimeStamp().getTime(); } - DataExtractorFactory.create(client, datafeedConfigHolder.get(), jobHolder.get(), xContentRegistry, dataExtractorFactoryHandler); + jobResultsProvider.datafeedTimingStats(jobHolder.get().getId(), datafeedTimingStatsHandler, listener::onFailure); }; // Collect data counts @@ -118,7 +121,8 @@ void build(String datafeedId, JobResultsProvider jobResultsProvider, JobConfigPr Consumer jobIdConsumer = jobId -> { BucketsQueryBuilder latestBucketQuery = new BucketsQueryBuilder() .sortField(Result.TIMESTAMP.getPreferredName()) - .sortDescending(true).size(1) + .sortDescending(true) + .size(1) .includeInterim(false); jobResultsProvider.bucketsViaInternalClient(jobId, latestBucketQuery, bucketsHandler, e -> { if (e instanceof ResourceNotFoundException) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedTimingStatsReporter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedTimingStatsReporter.java new file mode 100644 index 0000000000000..260db421e0446 --- /dev/null +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedTimingStatsReporter.java @@ -0,0 +1,88 @@ +/* + * 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.datafeed; + +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; + +import java.util.Objects; + +/** + * {@link DatafeedTimingStatsReporter} class handles the logic of persisting {@link DatafeedTimingStats} if they changed significantly + * since the last time they were persisted. + * + * This class is not thread-safe. + */ +public class DatafeedTimingStatsReporter { + + /** Persisted timing stats. May be stale. */ + private DatafeedTimingStats persistedTimingStats; + /** Current timing stats. */ + private volatile DatafeedTimingStats currentTimingStats; + /** Object used to persist current timing stats. */ + private final JobResultsPersister jobResultsPersister; + + public DatafeedTimingStatsReporter(DatafeedTimingStats timingStats, JobResultsPersister jobResultsPersister) { + Objects.requireNonNull(timingStats); + this.persistedTimingStats = new DatafeedTimingStats(timingStats); + this.currentTimingStats = new DatafeedTimingStats(timingStats); + this.jobResultsPersister = Objects.requireNonNull(jobResultsPersister); + } + + public DatafeedTimingStats getCurrentTimingStats() { + return new DatafeedTimingStats(currentTimingStats); + } + + /** + * Reports how much time did the search request execution take. + */ + public void reportSearchDuration(TimeValue searchDuration) { + if (searchDuration == null) { + return; + } + currentTimingStats.incrementTotalSearchTimeMs(searchDuration.millis()); + if (differSignificantly(currentTimingStats, persistedTimingStats)) { + // TODO: Consider changing refresh policy to NONE here and only do IMMEDIATE on datafeed _stop action + flush(WriteRequest.RefreshPolicy.IMMEDIATE); + } + } + + private void flush(WriteRequest.RefreshPolicy refreshPolicy) { + persistedTimingStats = new DatafeedTimingStats(currentTimingStats); + jobResultsPersister.persistDatafeedTimingStats(persistedTimingStats, refreshPolicy); + } + + /** + * Returns true if given stats objects differ from each other by more than 10% for at least one of the statistics. + */ + public static boolean differSignificantly(DatafeedTimingStats stats1, DatafeedTimingStats stats2) { + return differSignificantly(stats1.getTotalSearchTimeMs(), stats2.getTotalSearchTimeMs()); + } + + /** + * Returns {@code true} if one of the ratios { value1 / value2, value2 / value1 } is smaller than MIN_VALID_RATIO. + * This can be interpreted as values { value1, value2 } differing significantly from each other. + */ + private static boolean differSignificantly(double value1, double value2) { + return (value2 / value1 < MIN_VALID_RATIO) + || (value1 / value2 < MIN_VALID_RATIO) + || Math.abs(value1 - value2) > MAX_VALID_ABS_DIFFERENCE_MS; + } + + /** + * Minimum ratio of values that is interpreted as values being similar. + * If the values ratio is less than MIN_VALID_RATIO, the values are interpreted as significantly different. + */ + private static final double MIN_VALID_RATIO = 0.9; + + /** + * Maximum absolute difference of values that is interpreted as values being similar. + * If the values absolute difference is greater than MAX_VALID_ABS_DIFFERENCE, the values are interpreted as significantly different. + */ + private static final double MAX_VALID_ABS_DIFFERENCE_MS = 10000.0; // 10s +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactory.java index 948cea5de85d7..40e819affa087 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactory.java @@ -16,9 +16,10 @@ import org.elasticsearch.xpack.core.ml.datafeed.extractor.DataExtractor; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.rollup.action.GetRollupIndexCapsAction; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.aggregation.AggregationDataExtractorFactory; -import org.elasticsearch.xpack.ml.datafeed.extractor.chunked.ChunkedDataExtractorFactory; import org.elasticsearch.xpack.ml.datafeed.extractor.aggregation.RollupDataExtractorFactory; +import org.elasticsearch.xpack.ml.datafeed.extractor.chunked.ChunkedDataExtractorFactory; import org.elasticsearch.xpack.ml.datafeed.extractor.scroll.ScrollDataExtractorFactory; public interface DataExtractorFactory { @@ -31,10 +32,11 @@ static void create(Client client, DatafeedConfig datafeed, Job job, NamedXContentRegistry xContentRegistry, + DatafeedTimingStatsReporter timingStatsReporter, ActionListener listener) { ActionListener factoryHandler = ActionListener.wrap( factory -> listener.onResponse(datafeed.getChunkingConfig().isEnabled() - ? new ChunkedDataExtractorFactory(client, datafeed, job, xContentRegistry, factory) : factory) + ? new ChunkedDataExtractorFactory(client, datafeed, job, xContentRegistry, factory, timingStatsReporter) : factory) , listener::onFailure ); @@ -42,13 +44,15 @@ static void create(Client client, response -> { if (response.getJobs().isEmpty()) { // This means no rollup indexes are in the config if (datafeed.hasAggregations()) { - factoryHandler.onResponse(new AggregationDataExtractorFactory(client, datafeed, job, xContentRegistry)); + factoryHandler.onResponse( + new AggregationDataExtractorFactory(client, datafeed, job, xContentRegistry, timingStatsReporter)); } else { - ScrollDataExtractorFactory.create(client, datafeed, job, xContentRegistry, factoryHandler); + ScrollDataExtractorFactory.create(client, datafeed, job, xContentRegistry, timingStatsReporter, factoryHandler); } } else { if (datafeed.hasAggregations()) { // Rollup indexes require aggregations - RollupDataExtractorFactory.create(client, datafeed, job, response.getJobs(), xContentRegistry, factoryHandler); + RollupDataExtractorFactory.create( + client, datafeed, job, response.getJobs(), xContentRegistry, timingStatsReporter, factoryHandler); } else { listener.onFailure(new IllegalArgumentException("Aggregations are required when using Rollup indices")); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java index aa5c7ed6314b4..3559f3a7c50a2 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.ml.datafeed.extractor.DataExtractor; import org.elasticsearch.xpack.core.ml.datafeed.extractor.ExtractorUtils; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -50,17 +51,20 @@ abstract class AbstractAggregationDataExtractor { - AggregationDataExtractor(Client client, AggregationDataExtractorContext dataExtractorContext) { - super(client, dataExtractorContext); + AggregationDataExtractor( + Client client, AggregationDataExtractorContext dataExtractorContext, DatafeedTimingStatsReporter timingStatsReporter) { + super(client, dataExtractorContext, timingStatsReporter); } @Override diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactory.java index de205b276a049..40d186542df04 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactory.java @@ -9,9 +9,10 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.datafeed.extractor.DataExtractor; -import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.utils.Intervals; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; +import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; import java.util.Objects; @@ -21,12 +22,19 @@ public class AggregationDataExtractorFactory implements DataExtractorFactory { private final DatafeedConfig datafeedConfig; private final Job job; private final NamedXContentRegistry xContentRegistry; + private final DatafeedTimingStatsReporter timingStatsReporter; - public AggregationDataExtractorFactory(Client client, DatafeedConfig datafeedConfig, Job job, NamedXContentRegistry xContentRegistry) { + public AggregationDataExtractorFactory( + Client client, + DatafeedConfig datafeedConfig, + Job job, + NamedXContentRegistry xContentRegistry, + DatafeedTimingStatsReporter timingStatsReporter) { this.client = Objects.requireNonNull(client); this.datafeedConfig = Objects.requireNonNull(datafeedConfig); this.job = Objects.requireNonNull(job); this.xContentRegistry = xContentRegistry; + this.timingStatsReporter = Objects.requireNonNull(timingStatsReporter); } @Override @@ -43,6 +51,6 @@ public DataExtractor newExtractor(long start, long end) { Intervals.alignToFloor(end, histogramInterval), job.getAnalysisConfig().getSummaryCountFieldName().equals(DatafeedConfig.DOC_COUNT), datafeedConfig.getHeaders()); - return new AggregationDataExtractor(client, dataExtractorContext); + return new AggregationDataExtractor(client, dataExtractorContext, timingStatsReporter); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractor.java index f5de574e99a96..9341161057717 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractor.java @@ -9,6 +9,7 @@ import org.elasticsearch.client.Client; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.xpack.core.rollup.action.RollupSearchAction; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; /** * An implementation that extracts data from elasticsearch using search with aggregations against rollup indexes on a client. @@ -18,8 +19,9 @@ */ class RollupDataExtractor extends AbstractAggregationDataExtractor { - RollupDataExtractor(Client client, AggregationDataExtractorContext dataExtractorContext) { - super(client, dataExtractorContext); + RollupDataExtractor( + Client client, AggregationDataExtractorContext dataExtractorContext, DatafeedTimingStatsReporter timingStatsReporter) { + super(client, dataExtractorContext, timingStatsReporter); } @Override @@ -28,5 +30,4 @@ protected RollupSearchAction.RequestBuilder buildSearchRequest(SearchSourceBuild return new RollupSearchAction.RequestBuilder(client, searchRequest); } - } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractorFactory.java index 7a66ff49d62af..6c27fba4156f6 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractorFactory.java @@ -21,6 +21,7 @@ import org.elasticsearch.xpack.core.rollup.action.RollableIndexCaps; import org.elasticsearch.xpack.core.rollup.action.RollupJobCaps.RollupFieldCaps; import org.elasticsearch.xpack.core.rollup.job.DateHistogramGroupConfig; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; import java.time.ZoneId; @@ -45,12 +46,19 @@ public class RollupDataExtractorFactory implements DataExtractorFactory { private final DatafeedConfig datafeedConfig; private final Job job; private final NamedXContentRegistry xContentRegistry; - - private RollupDataExtractorFactory(Client client, DatafeedConfig datafeedConfig, Job job, NamedXContentRegistry xContentRegistry) { + private final DatafeedTimingStatsReporter timingStatsReporter; + + private RollupDataExtractorFactory( + Client client, + DatafeedConfig datafeedConfig, + Job job, + NamedXContentRegistry xContentRegistry, + DatafeedTimingStatsReporter timingStatsReporter) { this.client = Objects.requireNonNull(client); this.datafeedConfig = Objects.requireNonNull(datafeedConfig); this.job = Objects.requireNonNull(job); this.xContentRegistry = xContentRegistry; + this.timingStatsReporter = Objects.requireNonNull(timingStatsReporter); } @Override @@ -67,7 +75,7 @@ public DataExtractor newExtractor(long start, long end) { Intervals.alignToFloor(end, histogramInterval), job.getAnalysisConfig().getSummaryCountFieldName().equals(DatafeedConfig.DOC_COUNT), datafeedConfig.getHeaders()); - return new RollupDataExtractor(client, dataExtractorContext); + return new RollupDataExtractor(client, dataExtractorContext, timingStatsReporter); } public static void create(Client client, @@ -75,6 +83,7 @@ public static void create(Client client, Job job, Map rollupJobsWithCaps, NamedXContentRegistry xContentRegistry, + DatafeedTimingStatsReporter timingStatsReporter, ActionListener listener) { final AggregationBuilder datafeedHistogramAggregation = getHistogramAggregation( @@ -119,7 +128,7 @@ public static void create(Client client, return; } - listener.onResponse(new RollupDataExtractorFactory(client, datafeed, job, xContentRegistry)); + listener.onResponse(new RollupDataExtractorFactory(client, datafeed, job, xContentRegistry, timingStatsReporter)); } private static boolean validInterval(long datafeedInterval, ParsedRollupCaps rollupJobGroupConfig) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractor.java index f1e1fe2a10a32..c64aae1916b5a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractor.java @@ -23,6 +23,7 @@ import org.elasticsearch.xpack.core.ml.datafeed.extractor.DataExtractor; import org.elasticsearch.xpack.core.ml.datafeed.extractor.ExtractorUtils; import org.elasticsearch.xpack.core.rollup.action.RollupSearchAction; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; import org.elasticsearch.xpack.ml.datafeed.extractor.aggregation.RollupDataExtractorFactory; @@ -68,16 +69,22 @@ private interface DataSummary { private final DataExtractorFactory dataExtractorFactory; private final ChunkedDataExtractorContext context; private final DataSummaryFactory dataSummaryFactory; + private final DatafeedTimingStatsReporter timingStatsReporter; private long currentStart; private long currentEnd; private long chunkSpan; private boolean isCancelled; private DataExtractor currentExtractor; - public ChunkedDataExtractor(Client client, DataExtractorFactory dataExtractorFactory, ChunkedDataExtractorContext context) { + public ChunkedDataExtractor( + Client client, + DataExtractorFactory dataExtractorFactory, + ChunkedDataExtractorContext context, + DatafeedTimingStatsReporter timingStatsReporter) { this.client = Objects.requireNonNull(client); this.dataExtractorFactory = Objects.requireNonNull(dataExtractorFactory); this.context = Objects.requireNonNull(context); + this.timingStatsReporter = Objects.requireNonNull(timingStatsReporter); this.currentStart = context.start; this.currentEnd = context.start; this.isCancelled = false; @@ -198,15 +205,16 @@ private DataSummary buildDataSummary() throws IOException { private DataSummary newScrolledDataSummary() throws IOException { SearchRequestBuilder searchRequestBuilder = rangeSearchRequest(); - SearchResponse response = executeSearchRequest(searchRequestBuilder); + SearchResponse searchResponse = executeSearchRequest(searchRequestBuilder); LOGGER.debug("[{}] Scrolling Data summary response was obtained", context.jobId); + timingStatsReporter.reportSearchDuration(searchResponse.getTook()); - ExtractorUtils.checkSearchWasSuccessful(context.jobId, response); + ExtractorUtils.checkSearchWasSuccessful(context.jobId, searchResponse); - Aggregations aggregations = response.getAggregations(); + Aggregations aggregations = searchResponse.getAggregations(); long earliestTime = 0; long latestTime = 0; - long totalHits = response.getHits().getTotalHits().value; + long totalHits = searchResponse.getHits().getTotalHits().value; if (totalHits > 0) { Min min = aggregations.get(EARLIEST_TIME); earliestTime = (long) min.getValue(); @@ -220,12 +228,13 @@ private DataSummary newAggregatedDataSummary() throws IOException { // TODO: once RollupSearchAction is changed from indices:admin* to indices:data/read/* this branch is not needed ActionRequestBuilder searchRequestBuilder = dataExtractorFactory instanceof RollupDataExtractorFactory ? rollupRangeSearchRequest() : rangeSearchRequest(); - SearchResponse response = executeSearchRequest(searchRequestBuilder); + SearchResponse searchResponse = executeSearchRequest(searchRequestBuilder); LOGGER.debug("[{}] Aggregating Data summary response was obtained", context.jobId); + timingStatsReporter.reportSearchDuration(searchResponse.getTook()); - ExtractorUtils.checkSearchWasSuccessful(context.jobId, response); + ExtractorUtils.checkSearchWasSuccessful(context.jobId, searchResponse); - Aggregations aggregations = response.getAggregations(); + Aggregations aggregations = searchResponse.getAggregations(); Min min = aggregations.get(EARLIEST_TIME); Max max = aggregations.get(LATEST_TIME); return new AggregatedDataSummary(min.getValue(), max.getValue(), context.histogramInterval); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactory.java index fb8da71faa3fe..028409f69398b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactory.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.datafeed.extractor.DataExtractor; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.utils.Intervals; @@ -22,17 +23,20 @@ public class ChunkedDataExtractorFactory implements DataExtractorFactory { private final Job job; private final DataExtractorFactory dataExtractorFactory; private final NamedXContentRegistry xContentRegistry; + private final DatafeedTimingStatsReporter timingStatsReporter; public ChunkedDataExtractorFactory(Client client, DatafeedConfig datafeedConfig, Job job, NamedXContentRegistry xContentRegistry, - DataExtractorFactory dataExtractorFactory) { + DataExtractorFactory dataExtractorFactory, + DatafeedTimingStatsReporter timingStatsReporter) { this.client = Objects.requireNonNull(client); this.datafeedConfig = Objects.requireNonNull(datafeedConfig); this.job = Objects.requireNonNull(job); this.dataExtractorFactory = Objects.requireNonNull(dataExtractorFactory); this.xContentRegistry = xContentRegistry; + this.timingStatsReporter = Objects.requireNonNull(timingStatsReporter); } @Override @@ -52,7 +56,7 @@ public DataExtractor newExtractor(long start, long end) { datafeedConfig.hasAggregations(), datafeedConfig.hasAggregations() ? datafeedConfig.getHistogramIntervalMillis(xContentRegistry) : null ); - return new ChunkedDataExtractor(client, dataExtractorFactory, dataExtractorContext); + return new ChunkedDataExtractor(client, dataExtractorFactory, dataExtractorContext, timingStatsReporter); } private ChunkedDataExtractorContext.TimeAligner newTimeAligner() { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractor.java index dea775c24ca36..ea3bf44469862 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractor.java @@ -23,6 +23,7 @@ import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.ml.datafeed.extractor.DataExtractor; import org.elasticsearch.xpack.core.ml.datafeed.extractor.ExtractorUtils; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.fields.ExtractedField; import java.io.ByteArrayInputStream; @@ -47,6 +48,7 @@ class ScrollDataExtractor implements DataExtractor { private final Client client; private final ScrollDataExtractorContext context; + private final DatafeedTimingStatsReporter timingStatsReporter; private String scrollId; private boolean isCancelled; private boolean hasNext; @@ -54,9 +56,10 @@ class ScrollDataExtractor implements DataExtractor { protected Long lastTimestamp; private boolean searchHasShardFailure; - ScrollDataExtractor(Client client, ScrollDataExtractorContext dataExtractorContext) { + ScrollDataExtractor(Client client, ScrollDataExtractorContext dataExtractorContext, DatafeedTimingStatsReporter timingStatsReporter) { this.client = Objects.requireNonNull(client); - context = Objects.requireNonNull(dataExtractorContext); + this.context = Objects.requireNonNull(dataExtractorContext); + this.timingStatsReporter = Objects.requireNonNull(timingStatsReporter); hasNext = true; searchHasShardFailure = false; } @@ -109,6 +112,7 @@ protected InputStream initScroll(long startTimestamp) throws IOException { LOGGER.debug("[{}] Initializing scroll", context.jobId); SearchResponse searchResponse = executeSearchRequest(buildSearchRequest(startTimestamp)); LOGGER.debug("[{}] Search response was obtained", context.jobId); + timingStatsReporter.reportSearchDuration(searchResponse.getTook()); return processSearchResponse(searchResponse); } @@ -188,12 +192,14 @@ private InputStream continueScroll() throws IOException { if (searchHasShardFailure == false) { LOGGER.debug("[{}] Reinitializing scroll due to SearchPhaseExecutionException", context.jobId); markScrollAsErrored(); - searchResponse = executeSearchRequest(buildSearchRequest(lastTimestamp == null ? context.start : lastTimestamp)); + searchResponse = + executeSearchRequest(buildSearchRequest(lastTimestamp == null ? context.start : lastTimestamp)); } else { throw searchExecutionException; } } LOGGER.debug("[{}] Search response was obtained", context.jobId); + timingStatsReporter.reportSearchDuration(searchResponse.getTook()); return processSearchResponse(searchResponse); } @@ -209,7 +215,7 @@ private void markScrollAsErrored() { protected SearchResponse executeSearchScrollRequest(String scrollId) { return ClientHelper.executeWithHeaders(context.headers, ClientHelper.ML_ORIGIN, client, - () -> new SearchScrollRequestBuilder(client, SearchScrollAction.INSTANCE) + () -> new SearchScrollRequestBuilder(client, SearchScrollAction.INSTANCE) .setScroll(SCROLL_TIMEOUT) .setScrollId(scrollId) .get()); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java index ab912f54fe2a1..20ed7f664f92a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java @@ -19,6 +19,7 @@ import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import org.elasticsearch.xpack.core.ml.utils.MlStrings; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; import org.elasticsearch.xpack.ml.datafeed.extractor.fields.TimeBasedExtractedFields; @@ -31,14 +32,16 @@ public class ScrollDataExtractorFactory implements DataExtractorFactory { private final Job job; private final TimeBasedExtractedFields extractedFields; private final NamedXContentRegistry xContentRegistry; + private final DatafeedTimingStatsReporter timingStatsReporter; private ScrollDataExtractorFactory(Client client, DatafeedConfig datafeedConfig, Job job, TimeBasedExtractedFields extractedFields, - NamedXContentRegistry xContentRegistry) { + NamedXContentRegistry xContentRegistry, DatafeedTimingStatsReporter timingStatsReporter) { this.client = Objects.requireNonNull(client); this.datafeedConfig = Objects.requireNonNull(datafeedConfig); this.job = Objects.requireNonNull(job); this.extractedFields = Objects.requireNonNull(extractedFields); this.xContentRegistry = xContentRegistry; + this.timingStatsReporter = Objects.requireNonNull(timingStatsReporter); } @Override @@ -53,30 +56,33 @@ public DataExtractor newExtractor(long start, long end) { start, end, datafeedConfig.getHeaders()); - return new ScrollDataExtractor(client, dataExtractorContext); + return new ScrollDataExtractor(client, dataExtractorContext, timingStatsReporter); } public static void create(Client client, DatafeedConfig datafeed, Job job, NamedXContentRegistry xContentRegistry, - ActionListener listener ) { + DatafeedTimingStatsReporter timingStatsReporter, + ActionListener listener) { // Step 2. Contruct the factory and notify listener ActionListener fieldCapabilitiesHandler = ActionListener.wrap( - fieldCapabilitiesResponse -> { - TimeBasedExtractedFields extractedFields = TimeBasedExtractedFields.build(job, datafeed, fieldCapabilitiesResponse); - listener.onResponse(new ScrollDataExtractorFactory(client, datafeed, job, extractedFields, xContentRegistry)); - }, e -> { - if (e instanceof IndexNotFoundException) { - listener.onFailure(new ResourceNotFoundException("datafeed [" + datafeed.getId() - + "] cannot retrieve data because index " + ((IndexNotFoundException) e).getIndex() + " does not exist")); - } else if (e instanceof IllegalArgumentException) { - listener.onFailure(ExceptionsHelper.badRequestException("[" + datafeed.getId() + "] " + e.getMessage())); - } else { - listener.onFailure(e); - } + fieldCapabilitiesResponse -> { + TimeBasedExtractedFields extractedFields = TimeBasedExtractedFields.build(job, datafeed, fieldCapabilitiesResponse); + listener.onResponse( + new ScrollDataExtractorFactory(client, datafeed, job, extractedFields, xContentRegistry, timingStatsReporter)); + }, + e -> { + if (e instanceof IndexNotFoundException) { + listener.onFailure(new ResourceNotFoundException("datafeed [" + datafeed.getId() + + "] cannot retrieve data because index " + ((IndexNotFoundException) e).getIndex() + " does not exist")); + } else if (e instanceof IllegalArgumentException) { + listener.onFailure(ExceptionsHelper.badRequestException("[" + datafeed.getId() + "] " + e.getMessage())); + } else { + listener.onFailure(e); } + } ); // Step 1. Get field capabilities necessary to build the information of how to extract fields diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java index 8d43ec9b75df4..7c2f15591b94f 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java @@ -87,6 +87,7 @@ public class JobManager { private final Environment environment; private final JobResultsProvider jobResultsProvider; + private final JobResultsPersister jobResultsPersister; private final ClusterService clusterService; private final Auditor auditor; private final Client client; @@ -101,10 +102,11 @@ public class JobManager { * Create a JobManager */ public JobManager(Environment environment, Settings settings, JobResultsProvider jobResultsProvider, - ClusterService clusterService, Auditor auditor, ThreadPool threadPool, + JobResultsPersister jobResultsPersister, ClusterService clusterService, Auditor auditor, ThreadPool threadPool, Client client, UpdateJobProcessNotifier updateJobProcessNotifier, NamedXContentRegistry xContentRegistry) { this.environment = environment; this.jobResultsProvider = Objects.requireNonNull(jobResultsProvider); + this.jobResultsPersister = Objects.requireNonNull(jobResultsPersister); this.clusterService = Objects.requireNonNull(clusterService); this.auditor = Objects.requireNonNull(auditor); this.client = Objects.requireNonNull(client); @@ -573,12 +575,11 @@ public void revertSnapshot(RevertModelSnapshotAction.Request request, ActionList ModelSnapshot modelSnapshot) { final ModelSizeStats modelSizeStats = modelSnapshot.getModelSizeStats(); - final JobResultsPersister persister = new JobResultsPersister(client); // Step 3. After the model size stats is persisted, also persist the snapshot's quantiles and respond // ------- CheckedConsumer modelSizeStatsResponseHandler = response -> { - persister.persistQuantiles(modelSnapshot.getQuantiles(), WriteRequest.RefreshPolicy.IMMEDIATE, + jobResultsPersister.persistQuantiles(modelSnapshot.getQuantiles(), WriteRequest.RefreshPolicy.IMMEDIATE, ActionListener.wrap(quantilesResponse -> { // The quantiles can be large, and totally dominate the output - // it's clearer to remove them as they are not necessary for the revert op @@ -593,7 +594,7 @@ public void revertSnapshot(RevertModelSnapshotAction.Request request, ActionList CheckedConsumer updateHandler = response -> { if (response) { ModelSizeStats revertedModelSizeStats = new ModelSizeStats.Builder(modelSizeStats).setLogTime(new Date()).build(); - persister.persistModelSizeStats(revertedModelSizeStats, WriteRequest.RefreshPolicy.IMMEDIATE, ActionListener.wrap( + jobResultsPersister.persistModelSizeStats(revertedModelSizeStats, WriteRequest.RefreshPolicy.IMMEDIATE, ActionListener.wrap( modelSizeStatsResponseHandler, actionListener::onFailure)); } }; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleter.java index f48dcb977e59c..07c75f75f7e93 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleter.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleter.java @@ -21,6 +21,7 @@ import org.elasticsearch.index.reindex.BulkByScrollTask; import org.elasticsearch.index.reindex.DeleteByQueryAction; import org.elasticsearch.index.reindex.DeleteByQueryRequest; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.ModelSnapshot; import org.elasticsearch.xpack.core.ml.job.results.Result; @@ -121,6 +122,20 @@ public void deleteInterimResults() { LOGGER.error("[" + jobId + "] An error occurred while deleting interim results", e); } } + + /** + * Delete the datafeed timing stats document from all the job results indices + * + * @param listener Response listener + */ + public void deleteDatafeedTimingStats(ActionListener listener) { + DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest(AnomalyDetectorsIndex.jobResultsAliasedName(jobId)) + .setRefresh(true) + .setIndicesOptions(IndicesOptions.lenientExpandOpen()) + .setQuery(new IdsQueryBuilder().addIds(DatafeedTimingStats.documentId(jobId))); + + executeAsyncWithOrigin(client, ML_ORIGIN, DeleteByQueryAction.INSTANCE, deleteByQueryRequest, listener); + } // Wrapper to ensure safety private static class DeleteByQueryHolder { 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 b7d5214ecf304..1d960c5741836 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 @@ -23,6 +23,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; 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.ModelSizeStats; @@ -325,6 +326,20 @@ public void commitStateWrites(String jobId) { } } + /** + * Persist datafeed timing stats + * + * @param timingStats datafeed timing stats to persist + * @param refreshPolicy refresh policy to apply + */ + public IndexResponse persistDatafeedTimingStats(DatafeedTimingStats timingStats, WriteRequest.RefreshPolicy refreshPolicy) { + String jobId = timingStats.getJobId(); + logger.trace("[{}] Persisting datafeed timing stats", jobId); + Persistable persistable = new Persistable(jobId, timingStats, DatafeedTimingStats.documentId(timingStats.getJobId())); + persistable.setRefreshPolicy(refreshPolicy); + return persistable.persist(AnomalyDetectorsIndex.resultsWriteAlias(jobId)).actionGet(); + } + private XContentBuilder toXContentBuilder(ToXContent obj) throws IOException { XContentBuilder builder = jsonBuilder(); obj.toXContent(builder, ToXContent.EMPTY_PARAMS); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java index bb0ed2b3ddf0e..7e1426d9c20f5 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java @@ -24,6 +24,7 @@ import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.search.MultiSearchRequest; import org.elasticsearch.action.search.MultiSearchRequestBuilder; import org.elasticsearch.action.search.MultiSearchResponse; import org.elasticsearch.action.search.SearchRequest; @@ -81,6 +82,7 @@ import org.elasticsearch.xpack.core.ml.action.GetRecordsAction; import org.elasticsearch.xpack.core.ml.calendars.Calendar; import org.elasticsearch.xpack.core.ml.calendars.ScheduledEvent; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.config.MlFilter; import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; @@ -116,6 +118,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -428,18 +431,101 @@ public void timingStats(String jobId, Consumer handler, Consumer handler.accept(result.result), errorHandler, () -> new TimingStats(jobId)); } - private SearchRequestBuilder createTimingStatsSearch(String indexName, String jobId) { + private SearchRequestBuilder createLatestTimingStatsSearch(String indexName, String jobId) { return client.prepareSearch(indexName) .setSize(1) .setIndicesOptions(IndicesOptions.lenientExpandOpen()) - .setQuery(QueryBuilders.idsQuery().addIds(TimingStats.documentId(jobId))); + .setQuery(QueryBuilders.idsQuery().addIds(TimingStats.documentId(jobId))) + .addSort(SortBuilders.fieldSort(TimingStats.BUCKET_COUNT.getPreferredName()).order(SortOrder.DESC)); + } + + public void datafeedTimingStats(List jobIds, Consumer> handler, + Consumer errorHandler) { + if (jobIds.isEmpty()) { + handler.accept(Collections.emptyMap()); + return; + } + MultiSearchRequestBuilder msearchRequestBuilder = client.prepareMultiSearch(); + for (String jobId : jobIds) { + String indexName = AnomalyDetectorsIndex.jobResultsAliasedName(jobId); + msearchRequestBuilder.add(createLatestDatafeedTimingStatsSearch(indexName, jobId)); + } + MultiSearchRequest msearchRequest = msearchRequestBuilder.request(); + + executeAsyncWithOrigin( + client.threadPool().getThreadContext(), + ML_ORIGIN, + msearchRequest, + ActionListener.wrap( + msearchResponse -> { + Map timingStatsByJobId = new HashMap<>(); + for (int i = 0; i < msearchResponse.getResponses().length; i++) { + String jobId = jobIds.get(i); + MultiSearchResponse.Item itemResponse = msearchResponse.getResponses()[i]; + if (itemResponse.isFailure()) { + errorHandler.accept(itemResponse.getFailure()); + } else { + SearchResponse searchResponse = itemResponse.getResponse(); + ShardSearchFailure[] shardFailures = searchResponse.getShardFailures(); + int unavailableShards = searchResponse.getTotalShards() - searchResponse.getSuccessfulShards(); + if (shardFailures != null && shardFailures.length > 0) { + LOGGER.error("[{}] Search request returned shard failures: {}", jobId, Arrays.toString(shardFailures)); + errorHandler.accept( + new ElasticsearchException(ExceptionsHelper.shardFailuresToErrorMsg(jobId, shardFailures))); + } else if (unavailableShards > 0) { + errorHandler.accept( + new ElasticsearchException( + "[" + jobId + "] Search request encountered [" + unavailableShards + "] unavailable shards")); + } else { + SearchHits hits = searchResponse.getHits(); + long hitsCount = hits.getHits().length; + if (hitsCount == 0) { + SearchRequest searchRequest = msearchRequest.requests().get(i); + LOGGER.debug("Found 0 hits for [{}]", new Object[]{searchRequest.indices()}); + } else if (hitsCount > 1) { + SearchRequest searchRequest = msearchRequest.requests().get(i); + LOGGER.debug("Found multiple hits for [{}]", new Object[]{searchRequest.indices()}); + } else { + assert hitsCount == 1; + SearchHit hit = hits.getHits()[0]; + DatafeedTimingStats timingStats = parseSearchHit(hit, DatafeedTimingStats.PARSER, errorHandler); + timingStatsByJobId.put(jobId, timingStats); + } + } + } + } + handler.accept(timingStatsByJobId); + }, + errorHandler + ), + client::multiSearch); + } + + public void datafeedTimingStats(String jobId, Consumer handler, Consumer errorHandler) { + String indexName = AnomalyDetectorsIndex.jobResultsAliasedName(jobId); + searchSingleResult( + jobId, + DatafeedTimingStats.TYPE.getPreferredName(), + createLatestDatafeedTimingStatsSearch(indexName, jobId), + DatafeedTimingStats.PARSER, + result -> handler.accept(result.result), + errorHandler, + () -> new DatafeedTimingStats(jobId)); + } + + private SearchRequestBuilder createLatestDatafeedTimingStatsSearch(String indexName, String jobId) { + return client.prepareSearch(indexName) + .setSize(1) + .setIndicesOptions(IndicesOptions.lenientExpandOpen()) + .setQuery(QueryBuilders.idsQuery().addIds(DatafeedTimingStats.documentId(jobId))) + .addSort(SortBuilders.fieldSort(DatafeedTimingStats.TOTAL_SEARCH_TIME_MS.getPreferredName()).order(SortOrder.DESC)); } public void getAutodetectParams(Job job, Consumer consumer, Consumer errorHandler) { @@ -468,7 +554,7 @@ public void getAutodetectParams(Job job, Consumer consumer, Co MultiSearchRequestBuilder msearch = client.prepareMultiSearch() .add(createLatestDataCountsSearch(resultsIndex, jobId)) .add(createLatestModelSizeStatsSearch(resultsIndex)) - .add(createTimingStatsSearch(resultsIndex, jobId)) + .add(createLatestTimingStatsSearch(resultsIndex, jobId)) // These next two document IDs never need to be the legacy ones due to the rule // that you cannot open a 5.4 job in a subsequent version of the product .add(createDocIdSearch(resultsIndex, ModelSnapshot.documentId(jobId, job.getModelSnapshotId()))) @@ -525,7 +611,7 @@ private SearchRequestBuilder createDocIdSearch(String index, String id) { .setRouting(id); } - private void parseAutodetectParamSearchHit(String jobId, AutodetectParams.Builder paramsBuilder, SearchHit hit, + private static void parseAutodetectParamSearchHit(String jobId, AutodetectParams.Builder paramsBuilder, SearchHit hit, Consumer errorHandler) { String hitId = hit.getId(); if (DataCounts.documentId(jobId).equals(hitId)) { @@ -547,7 +633,7 @@ private void parseAutodetectParamSearchHit(String jobId, AutodetectParams.Builde } } - private T parseSearchHit(SearchHit hit, BiFunction objectParser, + private static T parseSearchHit(SearchHit hit, BiFunction objectParser, Consumer errorHandler) { BytesReference source = hit.getSourceRef(); try (InputStream stream = source.streamInput(); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/TimingStatsReporter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/TimingStatsReporter.java index d30335a5f06e9..0da4046edb49e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/TimingStatsReporter.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/TimingStatsReporter.java @@ -20,9 +20,9 @@ public class TimingStatsReporter { /** Persisted timing stats. May be stale. */ private TimingStats persistedTimingStats; /** Current timing stats. */ - private TimingStats currentTimingStats; + private volatile TimingStats currentTimingStats; /** Object used to persist current timing stats. */ - private JobResultsPersister.Builder bulkResultsPersister; + private final JobResultsPersister.Builder bulkResultsPersister; public TimingStatsReporter(TimingStats timingStats, JobResultsPersister.Builder jobResultsPersister) { Objects.requireNonNull(timingStats); @@ -50,7 +50,7 @@ public void finishReporting() { flush(); } - public void flush() { + private void flush() { persistedTimingStats = new TimingStats(currentTimingStats); bulkResultsPersister.persistTimingStats(persistedTimingStats); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobBuilderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobBuilderTests.java index 0c9b50c064f96..cd86241793240 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobBuilderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobBuilderTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.xpack.core.ml.job.results.Bucket; import org.elasticsearch.xpack.ml.datafeed.persistence.DatafeedConfigProvider; import org.elasticsearch.xpack.ml.job.persistence.JobConfigProvider; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; import org.elasticsearch.xpack.ml.job.persistence.JobResultsProvider; import org.elasticsearch.xpack.ml.notifications.Auditor; import org.junit.Before; @@ -48,6 +49,7 @@ public class DatafeedJobBuilderTests extends ESTestCase { private JobResultsProvider jobResultsProvider; private JobConfigProvider jobConfigProvider; private DatafeedConfigProvider datafeedConfigProvider; + private JobResultsPersister jobResultsPersister; private DatafeedJobBuilder datafeedJobBuilder; @@ -60,7 +62,7 @@ public void init() { when(client.settings()).thenReturn(Settings.EMPTY); auditor = mock(Auditor.class); taskHandler = mock(Consumer.class); - datafeedJobBuilder = new DatafeedJobBuilder(client, Settings.EMPTY, xContentRegistry(), auditor, System::currentTimeMillis); + jobResultsPersister = mock(JobResultsPersister.class); jobResultsProvider = mock(JobResultsProvider.class); Mockito.doAnswer(invocationOnMock -> { @@ -80,6 +82,16 @@ public void init() { jobConfigProvider = mock(JobConfigProvider.class); datafeedConfigProvider = mock(DatafeedConfigProvider.class); + datafeedJobBuilder = + new DatafeedJobBuilder( + client, + xContentRegistry(), + auditor, + System::currentTimeMillis, + jobConfigProvider, + jobResultsProvider, + datafeedConfigProvider, + jobResultsPersister); } public void testBuild_GivenScrollDatafeedAndNewJob() throws Exception { @@ -103,7 +115,7 @@ public void testBuild_GivenScrollDatafeedAndNewJob() throws Exception { givenJob(jobBuilder); givenDatafeed(datafeed); - datafeedJobBuilder.build("datafeed1", jobResultsProvider, jobConfigProvider, datafeedConfigProvider, datafeedJobHandler); + datafeedJobBuilder.build("datafeed1", datafeedJobHandler); assertBusy(() -> wasHandlerCalled.get()); } @@ -131,7 +143,7 @@ public void testBuild_GivenScrollDatafeedAndOldJobWithLatestRecordTimestampAfter givenJob(jobBuilder); givenDatafeed(datafeed); - datafeedJobBuilder.build("datafeed1", jobResultsProvider, jobConfigProvider, datafeedConfigProvider, datafeedJobHandler); + datafeedJobBuilder.build("datafeed1", datafeedJobHandler); assertBusy(() -> wasHandlerCalled.get()); } @@ -159,7 +171,7 @@ public void testBuild_GivenScrollDatafeedAndOldJobWithLatestBucketAfterLatestRec givenJob(jobBuilder); givenDatafeed(datafeed); - datafeedJobBuilder.build("datafeed1", jobResultsProvider, jobConfigProvider, datafeedConfigProvider, datafeedJobHandler); + datafeedJobBuilder.build("datafeed1", datafeedJobHandler); assertBusy(() -> wasHandlerCalled.get()); } @@ -184,8 +196,7 @@ public void testBuild_GivenBucketsRequestFails() { givenJob(jobBuilder); givenDatafeed(datafeed); - datafeedJobBuilder.build("datafeed1", jobResultsProvider, jobConfigProvider, datafeedConfigProvider, - ActionListener.wrap(datafeedJob -> fail(), taskHandler)); + datafeedJobBuilder.build("datafeed1", ActionListener.wrap(datafeedJob -> fail(), taskHandler)); verify(taskHandler).accept(error); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedTimingStatsReporterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedTimingStatsReporterTests.java new file mode 100644 index 0000000000000..9c86f05f2076e --- /dev/null +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedTimingStatsReporterTests.java @@ -0,0 +1,89 @@ +/* + * 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.datafeed; + +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; +import org.junit.Before; +import org.mockito.InOrder; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class DatafeedTimingStatsReporterTests extends ESTestCase { + + private static final String JOB_ID = "my-job-id"; + private static final TimeValue ONE_SECOND = TimeValue.timeValueSeconds(1); + + private JobResultsPersister jobResultsPersister; + + @Before + public void setUpTests() { + jobResultsPersister = mock(JobResultsPersister.class); + } + + public void testReportSearchDuration() { + DatafeedTimingStatsReporter timingStatsReporter = + new DatafeedTimingStatsReporter(new DatafeedTimingStats(JOB_ID, 3, 10000.0), jobResultsPersister); + assertThat(timingStatsReporter.getCurrentTimingStats(), equalTo(new DatafeedTimingStats(JOB_ID, 3, 10000.0))); + + timingStatsReporter.reportSearchDuration(ONE_SECOND); + assertThat(timingStatsReporter.getCurrentTimingStats(), equalTo(new DatafeedTimingStats(JOB_ID, 4, 11000.0))); + + timingStatsReporter.reportSearchDuration(ONE_SECOND); + assertThat(timingStatsReporter.getCurrentTimingStats(), equalTo(new DatafeedTimingStats(JOB_ID, 5, 12000.0))); + + timingStatsReporter.reportSearchDuration(ONE_SECOND); + assertThat(timingStatsReporter.getCurrentTimingStats(), equalTo(new DatafeedTimingStats(JOB_ID, 6, 13000.0))); + + timingStatsReporter.reportSearchDuration(ONE_SECOND); + assertThat(timingStatsReporter.getCurrentTimingStats(), equalTo(new DatafeedTimingStats(JOB_ID, 7, 14000.0))); + + InOrder inOrder = inOrder(jobResultsPersister); + inOrder.verify(jobResultsPersister).persistDatafeedTimingStats( + new DatafeedTimingStats(JOB_ID, 5, 12000.0), RefreshPolicy.IMMEDIATE); + inOrder.verify(jobResultsPersister).persistDatafeedTimingStats( + new DatafeedTimingStats(JOB_ID, 7, 14000.0), RefreshPolicy.IMMEDIATE); + verifyNoMoreInteractions(jobResultsPersister); + } + + public void testTimingStatsDifferSignificantly() { + assertThat( + DatafeedTimingStatsReporter.differSignificantly( + new DatafeedTimingStats(JOB_ID, 5, 1000.0), new DatafeedTimingStats(JOB_ID, 5, 1000.0)), + is(false)); + assertThat( + DatafeedTimingStatsReporter.differSignificantly( + new DatafeedTimingStats(JOB_ID, 5, 1000.0), new DatafeedTimingStats(JOB_ID, 5, 1100.0)), + is(false)); + assertThat( + DatafeedTimingStatsReporter.differSignificantly( + new DatafeedTimingStats(JOB_ID, 5, 1000.0), new DatafeedTimingStats(JOB_ID, 5, 1120.0)), + is(true)); + assertThat( + DatafeedTimingStatsReporter.differSignificantly( + new DatafeedTimingStats(JOB_ID, 5, 10000.0), new DatafeedTimingStats(JOB_ID, 5, 11000.0)), + is(false)); + assertThat( + DatafeedTimingStatsReporter.differSignificantly( + new DatafeedTimingStats(JOB_ID, 5, 10000.0), new DatafeedTimingStats(JOB_ID, 5, 11200.0)), + is(true)); + assertThat( + DatafeedTimingStatsReporter.differSignificantly( + new DatafeedTimingStats(JOB_ID, 5, 100000.0), new DatafeedTimingStats(JOB_ID, 5, 110000.0)), + is(false)); + assertThat( + DatafeedTimingStatsReporter.differSignificantly( + new DatafeedTimingStats(JOB_ID, 5, 100000.0), new DatafeedTimingStats(JOB_ID, 5, 110001.0)), + is(true)); + } +} diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactoryTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactoryTests.java index 62179f353001c..65859743a3534 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactoryTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactoryTests.java @@ -34,6 +34,7 @@ import org.elasticsearch.xpack.core.rollup.job.RollupJobConfig; import org.elasticsearch.xpack.core.rollup.job.TermsGroupConfig; import org.elasticsearch.xpack.ml.datafeed.DatafeedManagerTests; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.aggregation.AggregationDataExtractorFactory; import org.elasticsearch.xpack.ml.datafeed.extractor.chunked.ChunkedDataExtractorFactory; import org.elasticsearch.xpack.ml.datafeed.extractor.aggregation.RollupDataExtractorFactory; @@ -62,6 +63,7 @@ public class DataExtractorFactoryTests extends ESTestCase { private GetRollupIndexCapsAction.Response getRollupIndexResponse; private Client client; + private DatafeedTimingStatsReporter timingStatsReporter; @Override protected NamedXContentRegistry xContentRegistry() { @@ -72,6 +74,7 @@ protected NamedXContentRegistry xContentRegistry() { @Before public void setUpTests() { client = mock(Client.class); + timingStatsReporter = mock(DatafeedTimingStatsReporter.class); ThreadPool threadPool = mock(ThreadPool.class); when(client.threadPool()).thenReturn(threadPool); when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); @@ -109,7 +112,8 @@ public void testCreateDataExtractorFactoryGivenDefaultScroll() { e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig, jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig, jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenScrollWithAutoChunk() { @@ -125,7 +129,8 @@ public void testCreateDataExtractorFactoryGivenScrollWithAutoChunk() { e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenScrollWithOffChunk() { @@ -141,7 +146,8 @@ public void testCreateDataExtractorFactoryGivenScrollWithOffChunk() { e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenDefaultAggregation() { @@ -159,7 +165,8 @@ public void testCreateDataExtractorFactoryGivenDefaultAggregation() { e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenAggregationWithOffChunk() { @@ -178,7 +185,8 @@ public void testCreateDataExtractorFactoryGivenAggregationWithOffChunk() { e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenDefaultAggregationWithAutoChunk() { @@ -197,7 +205,8 @@ public void testCreateDataExtractorFactoryGivenDefaultAggregationWithAutoChunk() e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenRollupAndValidAggregation() { @@ -220,7 +229,8 @@ public void testCreateDataExtractorFactoryGivenRollupAndValidAggregation() { }, e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenRollupAndRemoteIndex() { @@ -246,7 +256,8 @@ public void testCreateDataExtractorFactoryGivenRollupAndRemoteIndex() { }, e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); // Test with remote index, aggregation, and chunking datafeedConfig.setChunkingConfig(ChunkingConfig.newAuto()); @@ -254,7 +265,8 @@ public void testCreateDataExtractorFactoryGivenRollupAndRemoteIndex() { dataExtractorFactory -> assertThat(dataExtractorFactory, instanceOf(ChunkedDataExtractorFactory.class)), e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); // Test with remote index, no aggregation, and no chunking datafeedConfig = DatafeedManagerTests.createDatafeedConfig("datafeed1", "foo"); @@ -266,7 +278,8 @@ public void testCreateDataExtractorFactoryGivenRollupAndRemoteIndex() { e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); // Test with remote index, no aggregation, and chunking datafeedConfig.setChunkingConfig(ChunkingConfig.newAuto()); @@ -274,7 +287,8 @@ public void testCreateDataExtractorFactoryGivenRollupAndRemoteIndex() { dataExtractorFactory -> assertThat(dataExtractorFactory, instanceOf(ChunkedDataExtractorFactory.class)), e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenRollupAndValidAggregationAndAutoChunk() { @@ -297,7 +311,8 @@ public void testCreateDataExtractorFactoryGivenRollupAndValidAggregationAndAutoC }, e -> fail() ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenRollupButNoAggregations() { @@ -317,7 +332,8 @@ public void testCreateDataExtractorFactoryGivenRollupButNoAggregations() { } ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenRollupWithBadInterval() { @@ -343,7 +359,8 @@ public void testCreateDataExtractorFactoryGivenRollupWithBadInterval() { assertWarnings("[interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future."); } ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenRollupMissingTerms() { @@ -368,7 +385,8 @@ public void testCreateDataExtractorFactoryGivenRollupMissingTerms() { assertWarnings("[interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future."); } ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } public void testCreateDataExtractorFactoryGivenRollupMissingMetric() { @@ -393,7 +411,8 @@ public void testCreateDataExtractorFactoryGivenRollupMissingMetric() { assertWarnings("[interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future."); } ); - DataExtractorFactory.create(client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), listener); + DataExtractorFactory.create( + client, datafeedConfig.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter, listener); } private void givenAggregatableRollup(String field, String type, int minuteInterval, String... groupByTerms) { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactoryTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactoryTests.java index fdfd75759db13..65e6e83babdd8 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactoryTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactoryTests.java @@ -17,6 +17,7 @@ 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.ml.datafeed.DatafeedTimingStatsReporter; import org.junit.Before; import java.util.Arrays; @@ -29,10 +30,12 @@ public class AggregationDataExtractorFactoryTests extends ESTestCase { private Client client; + private DatafeedTimingStatsReporter timingStatsReporter; @Before public void setUpMocks() { client = mock(Client.class); + timingStatsReporter = mock(DatafeedTimingStatsReporter.class); } @Override @@ -76,6 +79,7 @@ private AggregationDataExtractorFactory createFactory(long histogramInterval) { DatafeedConfig.Builder datafeedConfigBuilder = new DatafeedConfig.Builder("foo-feed", jobBuilder.getId()); datafeedConfigBuilder.setParsedAggregations(aggs); datafeedConfigBuilder.setIndices(Arrays.asList("my_index")); - return new AggregationDataExtractorFactory(client, datafeedConfigBuilder.build(), jobBuilder.build(new Date()), xContentRegistry()); + return new AggregationDataExtractorFactory( + client, datafeedConfigBuilder.build(), jobBuilder.build(new Date()), xContentRegistry(), timingStatsReporter); } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorTests.java index bb7d1d1729cd4..6d9db043755ad 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.client.Client; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.rest.RestStatus; @@ -17,7 +18,10 @@ import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.aggregation.AggregationTestUtils.Term; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; import org.junit.Before; import java.io.BufferedReader; @@ -54,13 +58,14 @@ public class AggregationDataExtractorTests extends ESTestCase { private List indices; private QueryBuilder query; private AggregatorFactories.Builder aggs; + private DatafeedTimingStatsReporter timingStatsReporter; private class TestDataExtractor extends AggregationDataExtractor { private SearchResponse nextResponse; TestDataExtractor(long start, long end) { - super(testClient, createContext(start, end)); + super(testClient, createContext(start, end), timingStatsReporter); } @Override @@ -88,6 +93,7 @@ public void setUpTests() { .addAggregator(AggregationBuilders.histogram("time").field("time").interval(1000).subAggregation( AggregationBuilders.terms("airline").field("airline").subAggregation( AggregationBuilders.avg("responsetime").field("responsetime")))); + timingStatsReporter = new DatafeedTimingStatsReporter(new DatafeedTimingStats(jobId), mock(JobResultsPersister.class)); } public void testExtraction() throws IOException { @@ -284,6 +290,7 @@ private SearchResponse createSearchResponse(Aggregations aggregations) { when(searchResponse.status()).thenReturn(RestStatus.OK); when(searchResponse.getScrollId()).thenReturn(randomAlphaOfLength(1000)); when(searchResponse.getAggregations()).thenReturn(aggregations); + when(searchResponse.getTook()).thenReturn(TimeValue.timeValueMillis(randomNonNegativeLong())); return searchResponse; } @@ -306,6 +313,7 @@ private SearchResponse createResponseWithUnavailableShards(int unavailableShards when(searchResponse.status()).thenReturn(RestStatus.OK); when(searchResponse.getSuccessfulShards()).thenReturn(3); when(searchResponse.getTotalShards()).thenReturn(3 + unavailableShards); + when(searchResponse.getTook()).thenReturn(TimeValue.timeValueMillis(randomNonNegativeLong())); return searchResponse; } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactoryTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactoryTests.java index 058e3c9e78645..b705b2eea7d22 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactoryTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactoryTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; import org.elasticsearch.xpack.core.ml.job.config.AnalysisConfig; import org.elasticsearch.xpack.core.ml.job.config.DataDescription; @@ -31,6 +32,7 @@ public class ChunkedDataExtractorFactoryTests extends ESTestCase { private Client client; private DataExtractorFactory dataExtractorFactory; + private DatafeedTimingStatsReporter timingStatsReporter; @Override protected NamedXContentRegistry xContentRegistry() { @@ -42,6 +44,7 @@ protected NamedXContentRegistry xContentRegistry() { public void setUpMocks() { client = mock(Client.class); dataExtractorFactory = mock(DataExtractorFactory.class); + timingStatsReporter = mock(DatafeedTimingStatsReporter.class); } public void testNewExtractor_GivenAlignedTimes() { @@ -103,7 +106,12 @@ private ChunkedDataExtractorFactory createFactory(long histogramInterval) { DatafeedConfig.Builder datafeedConfigBuilder = new DatafeedConfig.Builder("foo-feed", jobBuilder.getId()); datafeedConfigBuilder.setParsedAggregations(aggs); datafeedConfigBuilder.setIndices(Arrays.asList("my_index")); - return new ChunkedDataExtractorFactory(client, datafeedConfigBuilder.build(), jobBuilder.build(new Date()), - xContentRegistry(), dataExtractorFactory); + return new ChunkedDataExtractorFactory( + client, + datafeedConfigBuilder.build(), + jobBuilder.build(new Date()), + xContentRegistry(), + dataExtractorFactory, + timingStatsReporter); } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorTests.java index 406f1a5fa9024..77ebc9651ddd7 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorTests.java @@ -23,8 +23,11 @@ import org.elasticsearch.search.aggregations.metrics.Max; import org.elasticsearch.search.aggregations.metrics.Min; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; import org.elasticsearch.xpack.core.ml.datafeed.extractor.DataExtractor; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; import org.junit.Before; import java.io.IOException; @@ -54,17 +57,18 @@ public class ChunkedDataExtractorTests extends ESTestCase { private int scrollSize; private TimeValue chunkSpan; private DataExtractorFactory dataExtractorFactory; + private DatafeedTimingStatsReporter timingStatsReporter; private class TestDataExtractor extends ChunkedDataExtractor { private SearchResponse nextResponse; TestDataExtractor(long start, long end) { - super(client, dataExtractorFactory, createContext(start, end)); + super(client, dataExtractorFactory, createContext(start, end), timingStatsReporter); } TestDataExtractor(long start, long end, boolean hasAggregations, Long histogramInterval) { - super(client, dataExtractorFactory, createContext(start, end, hasAggregations, histogramInterval)); + super(client, dataExtractorFactory, createContext(start, end, hasAggregations, histogramInterval), timingStatsReporter); } @Override @@ -89,6 +93,7 @@ public void setUpTests() { scrollSize = 1000; chunkSpan = null; dataExtractorFactory = mock(DataExtractorFactory.class); + timingStatsReporter = new DatafeedTimingStatsReporter(new DatafeedTimingStats(jobId), mock(JobResultsPersister.class)); } public void testExtractionGivenNoData() throws IOException { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java index 131d47de38db6..eb1542ae814df 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java @@ -17,6 +17,7 @@ import org.elasticsearch.client.Client; import org.elasticsearch.common.document.DocumentField; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; @@ -28,8 +29,11 @@ import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; +import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.fields.ExtractedField; import org.elasticsearch.xpack.ml.datafeed.extractor.fields.TimeBasedExtractedFields; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; import org.junit.Before; import org.mockito.ArgumentCaptor; @@ -71,6 +75,7 @@ public class ScrollDataExtractorTests extends ESTestCase { private int scrollSize; private long initScrollStartTime; private ActionFuture clearScrollFuture; + private DatafeedTimingStatsReporter timingStatsReporter; private class TestDataExtractor extends ScrollDataExtractor { @@ -81,7 +86,7 @@ private class TestDataExtractor extends ScrollDataExtractor { } TestDataExtractor(ScrollDataExtractorContext context) { - super(client, context); + super(client, context, timingStatsReporter); } @Override @@ -140,6 +145,7 @@ public void setUpTests() { clearScrollFuture = mock(ActionFuture.class); capturedClearScrollRequests = ArgumentCaptor.forClass(ClearScrollRequest.class); when(client.execute(same(ClearScrollAction.INSTANCE), capturedClearScrollRequests.capture())).thenReturn(clearScrollFuture); + timingStatsReporter = new DatafeedTimingStatsReporter(new DatafeedTimingStats(jobId), mock(JobResultsPersister.class)); } public void testSinglePageExtraction() throws IOException { @@ -506,6 +512,7 @@ private SearchResponse createSearchResponse(List timestamps, List SearchHits searchHits = new SearchHits(hits.toArray(new SearchHit[0]), new TotalHits(hits.size(), TotalHits.Relation.EQUAL_TO), 1); when(searchResponse.getHits()).thenReturn(searchHits); + when(searchResponse.getTook()).thenReturn(TimeValue.timeValueMillis(randomNonNegativeLong())); return searchResponse; } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/JobManagerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/JobManagerTests.java index ed9709b2d48f5..458110b40907d 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/JobManagerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/JobManagerTests.java @@ -58,6 +58,7 @@ import org.elasticsearch.xpack.ml.MachineLearning; import org.elasticsearch.xpack.ml.MlConfigMigrationEligibilityCheck; import org.elasticsearch.xpack.ml.job.categorization.CategorizationAnalyzerTests; +import org.elasticsearch.xpack.ml.job.persistence.JobResultsPersister; import org.elasticsearch.xpack.ml.job.persistence.JobResultsProvider; import org.elasticsearch.xpack.ml.job.persistence.MockClientBuilder; import org.elasticsearch.xpack.ml.job.process.autodetect.UpdateParams; @@ -103,6 +104,7 @@ public class JobManagerTests extends ESTestCase { private ClusterService clusterService; private ThreadPool threadPool; private JobResultsProvider jobResultsProvider; + private JobResultsPersister jobResultsPersister; private Auditor auditor; private UpdateJobProcessNotifier updateJobProcessNotifier; @@ -123,6 +125,7 @@ public void setup() throws Exception { givenClusterSettings(settings); jobResultsProvider = mock(JobResultsProvider.class); + jobResultsPersister = mock(JobResultsPersister.class); auditor = mock(Auditor.class); updateJobProcessNotifier = mock(UpdateJobProcessNotifier.class); @@ -593,8 +596,17 @@ private Job.Builder createJob() { } private JobManager createJobManager(Client client) { - return new JobManager(environment, environment.settings(), jobResultsProvider, clusterService, - auditor, threadPool, client, updateJobProcessNotifier, xContentRegistry()); + return new JobManager( + environment, + environment.settings(), + jobResultsProvider, + jobResultsPersister, + clusterService, + auditor, + threadPool, + client, + updateJobProcessNotifier, + xContentRegistry()); } private ClusterState createClusterState() { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleterTests.java new file mode 100644 index 0000000000000..f02bc5bf9f1e7 --- /dev/null +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleterTests.java @@ -0,0 +1,57 @@ +/* + * 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.job.persistence; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.index.reindex.DeleteByQueryAction; +import org.elasticsearch.index.reindex.DeleteByQueryRequest; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; +import org.junit.Before; +import org.mockito.ArgumentCaptor; + +import static org.hamcrest.Matchers.arrayContaining; +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.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class JobDataDeleterTests extends ESTestCase { + + private static final String JOB_ID = "my-job-id"; + + private Client client; + + @Before + public void setUpTests() { + client = mock(Client.class); + ThreadPool threadPool = mock(ThreadPool.class); + when(client.threadPool()).thenReturn(threadPool); + when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); + } + + public void testDeleteDatafeedTimingStats() { + JobDataDeleter jobDataDeleter = new JobDataDeleter(client, JOB_ID); + jobDataDeleter.deleteDatafeedTimingStats(ActionListener.wrap( + deleteResponse -> {}, + e -> fail(e.toString()) + )); + + ArgumentCaptor deleteRequestCaptor = ArgumentCaptor.forClass(DeleteByQueryRequest.class); + verify(client).threadPool(); + verify(client).execute(eq(DeleteByQueryAction.INSTANCE), deleteRequestCaptor.capture(), any(ActionListener.class)); + verifyNoMoreInteractions(client); + + DeleteByQueryRequest deleteRequest = deleteRequestCaptor.getValue(); + assertThat(deleteRequest.indices(), arrayContaining(AnomalyDetectorsIndex.jobResultsAliasedName(JOB_ID))); + } +} 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 2d383bad1c405..0f3436daf8058 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,15 +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.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.TimingStats; import org.elasticsearch.xpack.core.ml.job.results.AnomalyRecord; import org.elasticsearch.xpack.core.ml.job.results.Bucket; @@ -33,6 +37,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -226,6 +231,38 @@ public void testPersistTimingStats() { verifyNoMoreInteractions(client); } + 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()); + return null; + }) + .when(client).index(any(), any(ActionListener.class)); + + JobResultsPersister persister = new JobResultsPersister(client); + DatafeedTimingStats timingStats = new DatafeedTimingStats("foo", 6, 666.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(); + assertThat(indexRequest.index(), equalTo(".ml-anomalies-.write-foo")); + assertThat(indexRequest.id(), equalTo("foo_datafeed_timing_stats")); + assertThat(indexRequest.getRefreshPolicy(), equalTo(WriteRequest.RefreshPolicy.IMMEDIATE)); + Map expectedSourceAsMap = new HashMap<>(); + expectedSourceAsMap.put("job_id", "foo"); + expectedSourceAsMap.put("search_count", 6); + expectedSourceAsMap.put("total_search_time_ms", 666.0); + assertThat(indexRequest.sourceAsMap(), equalTo(expectedSourceAsMap)); + + verify(client, times(1)).threadPool(); + verifyNoMoreInteractions(client); + } + @SuppressWarnings({"unchecked"}) private Client mockClient(ArgumentCaptor captor) { Client client = mock(Client.class); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProviderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProviderTests.java index 6f4d926889681..a292174f444df 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProviderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProviderTests.java @@ -11,7 +11,9 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; +import org.elasticsearch.action.search.MultiSearchAction; import org.elasticsearch.action.search.MultiSearchRequest; +import org.elasticsearch.action.search.MultiSearchRequestBuilder; import org.elasticsearch.action.search.MultiSearchResponse; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequest; @@ -42,6 +44,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.action.util.QueryPage; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedTimingStats; 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.AnomalyDetectorsIndexFields; @@ -67,7 +70,9 @@ import java.util.function.Consumer; import static org.elasticsearch.xpack.core.ml.job.config.JobTests.buildJobBuilder; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; @@ -75,6 +80,7 @@ import static org.mockito.Mockito.times; 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 JobResultsProviderTests extends ESTestCase { @@ -880,6 +886,121 @@ public void testTimingStats_NotFound() throws IOException { verifyNoMoreInteractions(client); } + public void testDatafeedTimingStats_EmptyJobList() { + Client client = getBasicMockedClient(); + + JobResultsProvider provider = createProvider(client); + provider.datafeedTimingStats( + Arrays.asList(), + statsByJobId -> assertThat(statsByJobId, anEmptyMap()), + e -> { throw new AssertionError(); }); + + verifyZeroInteractions(client); + } + + public void testDatafeedTimingStats_MultipleDocumentsAtOnce() throws IOException { + Map sourceFooMap = new HashMap<>(); + sourceFooMap.put(Job.ID.getPreferredName(), "foo"); + sourceFooMap.put(DatafeedTimingStats.SEARCH_COUNT.getPreferredName(), 6); + sourceFooMap.put(DatafeedTimingStats.TOTAL_SEARCH_TIME_MS.getPreferredName(), 666.0); + Map sourceBarMap = new HashMap<>(); + sourceBarMap.put(Job.ID.getPreferredName(), "bar"); + sourceBarMap.put(DatafeedTimingStats.SEARCH_COUNT.getPreferredName(), 7); + sourceBarMap.put(DatafeedTimingStats.TOTAL_SEARCH_TIME_MS.getPreferredName(), 777.0); + + List> sourceFoo = Arrays.asList(sourceFooMap); + List> sourceBar = Arrays.asList(sourceBarMap); + + SearchResponse responseFoo = createSearchResponse(sourceFoo); + SearchResponse responseBar = createSearchResponse(sourceBar); + MultiSearchResponse multiSearchResponse = new MultiSearchResponse( + new MultiSearchResponse.Item[]{ + new MultiSearchResponse.Item(responseFoo, null), + new MultiSearchResponse.Item(responseBar, null)}, + randomNonNegativeLong()); + + Client client = getBasicMockedClient(); + when(client.prepareMultiSearch()).thenReturn(new MultiSearchRequestBuilder(client, MultiSearchAction.INSTANCE)); + doAnswer(invocationOnMock -> { + MultiSearchRequest multiSearchRequest = (MultiSearchRequest) invocationOnMock.getArguments()[0]; + assertThat(multiSearchRequest.requests(), hasSize(2)); + assertThat(multiSearchRequest.requests().get(0).source().query().getName(), equalTo("ids")); + assertThat(multiSearchRequest.requests().get(1).source().query().getName(), equalTo("ids")); + @SuppressWarnings("unchecked") + ActionListener actionListener = (ActionListener) invocationOnMock.getArguments()[1]; + actionListener.onResponse(multiSearchResponse); + return null; + }).when(client).multiSearch(any(), any()); + when(client.prepareSearch(AnomalyDetectorsIndex.jobResultsAliasedName("foo"))) + .thenReturn( + new SearchRequestBuilder(client, SearchAction.INSTANCE).setIndices(AnomalyDetectorsIndex.jobResultsAliasedName("foo"))); + when(client.prepareSearch(AnomalyDetectorsIndex.jobResultsAliasedName("bar"))) + .thenReturn( + new SearchRequestBuilder(client, SearchAction.INSTANCE).setIndices(AnomalyDetectorsIndex.jobResultsAliasedName("bar"))); + + Map expectedStatsByJobId = new HashMap<>(); + expectedStatsByJobId.put("foo", new DatafeedTimingStats("foo", 6, 666.0)); + expectedStatsByJobId.put("bar", new DatafeedTimingStats("bar", 7, 777.0)); + JobResultsProvider provider = createProvider(client); + provider.datafeedTimingStats( + Arrays.asList("foo", "bar"), + statsByJobId -> assertThat(statsByJobId, equalTo(expectedStatsByJobId)), + e -> { throw new AssertionError(); }); + + verify(client).threadPool(); + verify(client).prepareMultiSearch(); + verify(client).multiSearch(any(MultiSearchRequest.class), any(ActionListener.class)); + verify(client).prepareSearch(AnomalyDetectorsIndex.jobResultsAliasedName("foo")); + verify(client).prepareSearch(AnomalyDetectorsIndex.jobResultsAliasedName("bar")); + verifyNoMoreInteractions(client); + } + + public void testDatafeedTimingStats_Ok() throws IOException { + String indexName = AnomalyDetectorsIndex.jobResultsAliasedName("foo"); + Map sourceFooMap = new HashMap<>(); + sourceFooMap.put(Job.ID.getPreferredName(), "foo"); + sourceFooMap.put(DatafeedTimingStats.SEARCH_COUNT.getPreferredName(), 6); + sourceFooMap.put(DatafeedTimingStats.TOTAL_SEARCH_TIME_MS.getPreferredName(), 666.0); + List> source = Arrays.asList(sourceFooMap); + SearchResponse response = createSearchResponse(source); + Client client = getMockedClient( + queryBuilder -> assertThat(queryBuilder.getName(), equalTo("ids")), + response); + + when(client.prepareSearch(indexName)).thenReturn(new SearchRequestBuilder(client, SearchAction.INSTANCE).setIndices(indexName)); + JobResultsProvider provider = createProvider(client); + provider.datafeedTimingStats( + "foo", + stats -> assertThat(stats, equalTo(new DatafeedTimingStats("foo", 6, 666.0))), + e -> { throw new AssertionError(); }); + + verify(client).prepareSearch(indexName); + verify(client).threadPool(); + verify(client).search(any(SearchRequest.class), any(ActionListener.class)); + verifyNoMoreInteractions(client); + } + + public void testDatafeedTimingStats_NotFound() throws IOException { + String indexName = AnomalyDetectorsIndex.jobResultsAliasedName("foo"); + List> source = new ArrayList<>(); + SearchResponse response = createSearchResponse(source); + Client client = getMockedClient( + queryBuilder -> assertThat(queryBuilder.getName(), equalTo("ids")), + response); + + when(client.prepareSearch(indexName)).thenReturn(new SearchRequestBuilder(client, SearchAction.INSTANCE).setIndices(indexName)); + JobResultsProvider provider = createProvider(client); + provider.datafeedTimingStats( + "foo", + stats -> assertThat(stats, equalTo(new DatafeedTimingStats("foo"))), + e -> { throw new AssertionError(); }); + + verify(client).prepareSearch(indexName); + verify(client).threadPool(); + verify(client).search(any(SearchRequest.class), any(ActionListener.class)); + verifyNoMoreInteractions(client); + } + private Bucket createBucketAtEpochTime(long epoch) { return new Bucket("foo", new Date(epoch), 123); } @@ -910,11 +1031,16 @@ private static SearchResponse createSearchResponse(List> sou return response; } - private Client getMockedClient(Consumer queryBuilderConsumer, SearchResponse response) { + private Client getBasicMockedClient() { Client client = mock(Client.class); ThreadPool threadPool = mock(ThreadPool.class); when(client.threadPool()).thenReturn(threadPool); when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); + return client; + } + + private Client getMockedClient(Consumer queryBuilderConsumer, SearchResponse response) { + Client client = getBasicMockedClient(); doAnswer(invocationOnMock -> { MultiSearchRequest multiSearchRequest = (MultiSearchRequest) invocationOnMock.getArguments()[0]; queryBuilderConsumer.accept(multiSearchRequest.requests().get(0).source().query()); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/TimingStatsReporterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/TimingStatsReporterTests.java index f2314e6de3ed8..4e5a97f860d9d 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/TimingStatsReporterTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/TimingStatsReporterTests.java @@ -55,7 +55,7 @@ public void testReporting() { inOrder.verifyNoMoreInteractions(); } - public void testFlush() { + public void testFinishReporting() { TimingStatsReporter reporter = new TimingStatsReporter(new TimingStats(JOB_ID), bulkResultsPersister); assertThat(reporter.getCurrentTimingStats(), equalTo(new TimingStats(JOB_ID))); @@ -68,7 +68,7 @@ public void testFlush() { reporter.reportBucketProcessingTime(10); assertThat(reporter.getCurrentTimingStats(), equalTo(new TimingStats(JOB_ID, 3, 10.0, 10.0, 10.0, 10.0))); - reporter.flush(); + reporter.finishReporting(); assertThat(reporter.getCurrentTimingStats(), equalTo(new TimingStats(JOB_ID, 3, 10.0, 10.0, 10.0, 10.0))); InOrder inOrder = inOrder(bulkResultsPersister); diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/get_datafeed_stats.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/get_datafeed_stats.yml index f9178dc6f71f5..aa0e023e64054 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/get_datafeed_stats.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/get_datafeed_stats.yml @@ -126,16 +126,22 @@ setup: - do: ml.get_datafeed_stats: datafeed_id: datafeed-1 - - match: { datafeeds.0.datafeed_id: "datafeed-1"} - - match: { datafeeds.0.state: "stopped"} + - match: { datafeeds.0.datafeed_id: "datafeed-1"} + - match: { datafeeds.0.state: "stopped"} - is_false: datafeeds.0.node + - match: { datafeeds.0.timing_stats.job_id: "get-datafeed-stats-1" } + - match: { datafeeds.0.timing_stats.search_count: 0 } + - match: { datafeeds.0.timing_stats.total_search_time_ms: 0.0} - do: ml.get_datafeed_stats: datafeed_id: datafeed-2 - - match: { datafeeds.0.datafeed_id: "datafeed-2"} - - match: { datafeeds.0.state: "stopped"} + - match: { datafeeds.0.datafeed_id: "datafeed-2"} + - match: { datafeeds.0.state: "stopped"} - is_false: datafeeds.0.node + - match: { datafeeds.0.timing_stats.job_id: "get-datafeed-stats-2" } + - match: { datafeeds.0.timing_stats.search_count: 0 } + - match: { datafeeds.0.timing_stats.total_search_time_ms: 0.0} --- "Test get stats for started datafeed": @@ -146,8 +152,8 @@ setup: - do: ml.start_datafeed: - "datafeed_id": "datafeed-1" - "start": 0 + datafeed_id: "datafeed-1" + start: 0 - do: ml.get_datafeed_stats: @@ -158,6 +164,42 @@ setup: - is_true: datafeeds.0.node.transport_address - match: { datafeeds.0.node.attributes.ml\.max_open_jobs: "20"} +--- +"Test get stats for started datafeed contains timing stats": + + - do: + ml.open_job: + job_id: get-datafeed-stats-1 + + - do: + ml.start_datafeed: + datafeed_id: "datafeed-1" + start: 0 + + - do: + ml.get_datafeed_stats: + datafeed_id: datafeed-1 + - match: { datafeeds.0.datafeed_id: "datafeed-1"} + - match: { datafeeds.0.state: "started"} + - match: { datafeeds.0.timing_stats.job_id: "get-datafeed-stats-1"} + - match: { datafeeds.0.timing_stats.search_count: 0} + - match: { datafeeds.0.timing_stats.total_search_time_ms: 0.0} + + - do: + ml.stop_datafeed: + datafeed_id: "datafeed-1" + - match: { stopped: true} + + - do: + ml.get_datafeed_stats: + datafeed_id: datafeed-1 + - match: { datafeeds.0.datafeed_id: "datafeed-1"} + - match: { datafeeds.0.state: "stopped"} + - match: { datafeeds.0.timing_stats.job_id: "get-datafeed-stats-1"} + # TODO: Change "gte 0" to "match 1" once https://github.com/elastic/elasticsearch/issues/44132 is fixed + - gte: { datafeeds.0.timing_stats.search_count: 0} + - gte: { datafeeds.0.timing_stats.total_search_time_ms: 0.0} + --- "Test implicit get all datafeed stats given started datafeeds": @@ -167,8 +209,8 @@ setup: - do: ml.start_datafeed: - "datafeed_id": "datafeed-1" - "start": 0 + datafeed_id: "datafeed-1" + start: 0 - do: ml.open_job: @@ -176,8 +218,8 @@ setup: - do: ml.start_datafeed: - "datafeed_id": "datafeed-2" - "start": 0 + datafeed_id: "datafeed-2" + start: 0 - do: ml.get_datafeed_stats: {}