Skip to content

Commit be554fe

Browse files
[7.x][ML] Improve progress reportings for DF analytics (#45856) (#45910)
Previously, the stats API reports a progress percentage for DF analytics tasks that are running and are in the `reindexing` or `analyzing` state. This means that when the task is `stopped` there is no progress reported. Thus, one cannot distinguish between a task that never run to one that completed. In addition, there are blind spots in the progress reporting. In particular, we do not account for when data is loaded into the process. We also do not account for when results are written. This commit addresses the above issues. It changes progress to being a list of objects, each one describing the phase and its progress as a percentage. We currently have 4 phases: reindexing, loading_data, analyzing, writing_results. When the task stops, progress is persisted as a document in the state index. The stats API now reports progress from in-memory if the task is running, or returns the persisted document (if there is one).
1 parent b756e1b commit be554fe

File tree

27 files changed

+989
-129
lines changed

27 files changed

+989
-129
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsStats.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.common.xcontent.XContentParser;
2929

3030
import java.io.IOException;
31+
import java.util.List;
3132
import java.util.Objects;
3233

3334
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
@@ -42,17 +43,18 @@ public static DataFrameAnalyticsStats fromXContent(XContentParser parser) throws
4243
static final ParseField ID = new ParseField("id");
4344
static final ParseField STATE = new ParseField("state");
4445
static final ParseField FAILURE_REASON = new ParseField("failure_reason");
45-
static final ParseField PROGRESS_PERCENT = new ParseField("progress_percent");
46+
static final ParseField PROGRESS = new ParseField("progress");
4647
static final ParseField NODE = new ParseField("node");
4748
static final ParseField ASSIGNMENT_EXPLANATION = new ParseField("assignment_explanation");
4849

50+
@SuppressWarnings("unchecked")
4951
private static final ConstructingObjectParser<DataFrameAnalyticsStats, Void> PARSER =
5052
new ConstructingObjectParser<>("data_frame_analytics_stats", true,
5153
args -> new DataFrameAnalyticsStats(
5254
(String) args[0],
5355
(DataFrameAnalyticsState) args[1],
5456
(String) args[2],
55-
(Integer) args[3],
57+
(List<PhaseProgress>) args[3],
5658
(NodeAttributes) args[4],
5759
(String) args[5]));
5860

@@ -65,25 +67,25 @@ public static DataFrameAnalyticsStats fromXContent(XContentParser parser) throws
6567
throw new IllegalArgumentException("Unsupported token [" + p.currentToken() + "]");
6668
}, STATE, ObjectParser.ValueType.STRING);
6769
PARSER.declareString(optionalConstructorArg(), FAILURE_REASON);
68-
PARSER.declareInt(optionalConstructorArg(), PROGRESS_PERCENT);
70+
PARSER.declareObjectArray(optionalConstructorArg(), PhaseProgress.PARSER, PROGRESS);
6971
PARSER.declareObject(optionalConstructorArg(), NodeAttributes.PARSER, NODE);
7072
PARSER.declareString(optionalConstructorArg(), ASSIGNMENT_EXPLANATION);
7173
}
7274

7375
private final String id;
7476
private final DataFrameAnalyticsState state;
7577
private final String failureReason;
76-
private final Integer progressPercent;
78+
private final List<PhaseProgress> progress;
7779
private final NodeAttributes node;
7880
private final String assignmentExplanation;
7981

8082
public DataFrameAnalyticsStats(String id, DataFrameAnalyticsState state, @Nullable String failureReason,
81-
@Nullable Integer progressPercent, @Nullable NodeAttributes node,
83+
@Nullable List<PhaseProgress> progress, @Nullable NodeAttributes node,
8284
@Nullable String assignmentExplanation) {
8385
this.id = id;
8486
this.state = state;
8587
this.failureReason = failureReason;
86-
this.progressPercent = progressPercent;
88+
this.progress = progress;
8789
this.node = node;
8890
this.assignmentExplanation = assignmentExplanation;
8991
}
@@ -100,8 +102,8 @@ public String getFailureReason() {
100102
return failureReason;
101103
}
102104

103-
public Integer getProgressPercent() {
104-
return progressPercent;
105+
public List<PhaseProgress> getProgress() {
106+
return progress;
105107
}
106108

107109
public NodeAttributes getNode() {
@@ -121,14 +123,14 @@ public boolean equals(Object o) {
121123
return Objects.equals(id, other.id)
122124
&& Objects.equals(state, other.state)
123125
&& Objects.equals(failureReason, other.failureReason)
124-
&& Objects.equals(progressPercent, other.progressPercent)
126+
&& Objects.equals(progress, other.progress)
125127
&& Objects.equals(node, other.node)
126128
&& Objects.equals(assignmentExplanation, other.assignmentExplanation);
127129
}
128130

129131
@Override
130132
public int hashCode() {
131-
return Objects.hash(id, state, failureReason, progressPercent, node, assignmentExplanation);
133+
return Objects.hash(id, state, failureReason, progress, node, assignmentExplanation);
132134
}
133135

134136
@Override
@@ -137,7 +139,7 @@ public String toString() {
137139
.add("id", id)
138140
.add("state", state)
139141
.add("failureReason", failureReason)
140-
.add("progressPercent", progressPercent)
142+
.add("progress", progress)
141143
.add("node", node)
142144
.add("assignmentExplanation", assignmentExplanation)
143145
.toString();
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.client.ml.dataframe;
20+
21+
import org.elasticsearch.common.ParseField;
22+
import org.elasticsearch.common.inject.internal.ToStringBuilder;
23+
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
24+
import org.elasticsearch.common.xcontent.ToXContentObject;
25+
import org.elasticsearch.common.xcontent.XContentBuilder;
26+
27+
import java.io.IOException;
28+
import java.util.Objects;
29+
30+
/**
31+
* A class that describes a phase and its progress as a percentage
32+
*/
33+
public class PhaseProgress implements ToXContentObject {
34+
35+
static final ParseField PHASE = new ParseField("phase");
36+
static final ParseField PROGRESS_PERCENT = new ParseField("progress_percent");
37+
38+
public static final ConstructingObjectParser<PhaseProgress, Void> PARSER = new ConstructingObjectParser<>("phase_progress",
39+
true, a -> new PhaseProgress((String) a[0], (int) a[1]));
40+
41+
static {
42+
PARSER.declareString(ConstructingObjectParser.constructorArg(), PHASE);
43+
PARSER.declareInt(ConstructingObjectParser.constructorArg(), PROGRESS_PERCENT);
44+
}
45+
46+
private final String phase;
47+
private final int progressPercent;
48+
49+
public PhaseProgress(String phase, int progressPercent) {
50+
this.phase = Objects.requireNonNull(phase);
51+
this.progressPercent = progressPercent;
52+
}
53+
54+
public String getPhase() {
55+
return phase;
56+
}
57+
58+
public int getProgressPercent() {
59+
return progressPercent;
60+
}
61+
62+
@Override
63+
public int hashCode() {
64+
return Objects.hash(phase, progressPercent);
65+
}
66+
67+
@Override
68+
public boolean equals(Object o) {
69+
if (this == o) return true;
70+
if (o == null || getClass() != o.getClass()) return false;
71+
PhaseProgress that = (PhaseProgress) o;
72+
return Objects.equals(phase, that.phase) && progressPercent == that.progressPercent;
73+
}
74+
75+
@Override
76+
public String toString() {
77+
return new ToStringBuilder(getClass())
78+
.add(PHASE.getPreferredName(), phase)
79+
.add(PROGRESS_PERCENT.getPreferredName(), progressPercent)
80+
.toString();
81+
}
82+
83+
@Override
84+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
85+
builder.startObject();
86+
builder.field(PhaseProgress.PHASE.getPreferredName(), phase);
87+
builder.field(PhaseProgress.PROGRESS_PERCENT.getPreferredName(), progressPercent);
88+
builder.endObject();
89+
return builder;
90+
}
91+
}

client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
import org.elasticsearch.client.ml.dataframe.DataFrameAnalyticsState;
124124
import org.elasticsearch.client.ml.dataframe.DataFrameAnalyticsStats;
125125
import org.elasticsearch.client.ml.dataframe.OutlierDetection;
126+
import org.elasticsearch.client.ml.dataframe.PhaseProgress;
126127
import org.elasticsearch.client.ml.dataframe.QueryConfig;
127128
import org.elasticsearch.client.ml.dataframe.evaluation.regression.MeanSquaredErrorMetric;
128129
import org.elasticsearch.client.ml.dataframe.evaluation.regression.RSquaredMetric;
@@ -1405,11 +1406,17 @@ public void testGetDataFrameAnalyticsStats() throws Exception {
14051406
assertThat(stats.getId(), equalTo(configId));
14061407
assertThat(stats.getState(), equalTo(DataFrameAnalyticsState.STOPPED));
14071408
assertNull(stats.getFailureReason());
1408-
assertNull(stats.getProgressPercent());
14091409
assertNull(stats.getNode());
14101410
assertNull(stats.getAssignmentExplanation());
14111411
assertThat(statsResponse.getNodeFailures(), hasSize(0));
14121412
assertThat(statsResponse.getTaskFailures(), hasSize(0));
1413+
List<PhaseProgress> progress = stats.getProgress();
1414+
assertThat(progress, is(notNullValue()));
1415+
assertThat(progress.size(), equalTo(4));
1416+
assertThat(progress.get(0), equalTo(new PhaseProgress("reindexing", 0)));
1417+
assertThat(progress.get(1), equalTo(new PhaseProgress("loading_data", 0)));
1418+
assertThat(progress.get(2), equalTo(new PhaseProgress("analyzing", 0)));
1419+
assertThat(progress.get(3), equalTo(new PhaseProgress("writing_results", 0)));
14131420
}
14141421

14151422
public void testStartDataFrameAnalyticsConfig() throws Exception {

client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsStatsTests.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.elasticsearch.test.ESTestCase;
2525

2626
import java.io.IOException;
27+
import java.util.ArrayList;
28+
import java.util.List;
2729

2830
import static org.elasticsearch.test.AbstractXContentTestCase.xContentTester;
2931

@@ -44,20 +46,29 @@ public static DataFrameAnalyticsStats randomDataFrameAnalyticsStats() {
4446
randomAlphaOfLengthBetween(1, 10),
4547
randomFrom(DataFrameAnalyticsState.values()),
4648
randomBoolean() ? null : randomAlphaOfLength(10),
47-
randomBoolean() ? null : randomIntBetween(0, 100),
49+
randomBoolean() ? null : createRandomProgress(),
4850
randomBoolean() ? null : NodeAttributesTests.createRandom(),
4951
randomBoolean() ? null : randomAlphaOfLengthBetween(1, 20));
5052
}
5153

54+
private static List<PhaseProgress> createRandomProgress() {
55+
int progressPhaseCount = randomIntBetween(3, 7);
56+
List<PhaseProgress> progress = new ArrayList<>(progressPhaseCount);
57+
for (int i = 0; i < progressPhaseCount; i++) {
58+
progress.add(new PhaseProgress(randomAlphaOfLength(20), randomIntBetween(0, 100)));
59+
}
60+
return progress;
61+
}
62+
5263
public static void toXContent(DataFrameAnalyticsStats stats, XContentBuilder builder) throws IOException {
5364
builder.startObject();
5465
builder.field(DataFrameAnalyticsStats.ID.getPreferredName(), stats.getId());
5566
builder.field(DataFrameAnalyticsStats.STATE.getPreferredName(), stats.getState().value());
5667
if (stats.getFailureReason() != null) {
5768
builder.field(DataFrameAnalyticsStats.FAILURE_REASON.getPreferredName(), stats.getFailureReason());
5869
}
59-
if (stats.getProgressPercent() != null) {
60-
builder.field(DataFrameAnalyticsStats.PROGRESS_PERCENT.getPreferredName(), stats.getProgressPercent());
70+
if (stats.getProgress() != null) {
71+
builder.field(DataFrameAnalyticsStats.PROGRESS.getPreferredName(), stats.getProgress());
6172
}
6273
if (stats.getNode() != null) {
6374
builder.field(DataFrameAnalyticsStats.NODE.getPreferredName(), stats.getNode());
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.client.ml.dataframe;
20+
21+
import org.elasticsearch.common.xcontent.XContentParser;
22+
import org.elasticsearch.test.AbstractXContentTestCase;
23+
24+
import java.io.IOException;
25+
26+
public class PhaseProgressTests extends AbstractXContentTestCase<PhaseProgress> {
27+
28+
public static PhaseProgress createRandom() {
29+
return new PhaseProgress(randomAlphaOfLength(20), randomIntBetween(0, 100));
30+
}
31+
32+
@Override
33+
protected PhaseProgress createTestInstance() {
34+
return createRandom();
35+
}
36+
37+
@Override
38+
protected PhaseProgress doParseInstance(XContentParser parser) throws IOException {
39+
return PhaseProgress.PARSER.apply(parser, null);
40+
}
41+
42+
@Override
43+
protected boolean supportsUnknownFields() {
44+
return true;
45+
}
46+
}

docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,25 @@ The API returns the following results:
9999
"data_frame_analytics": [
100100
{
101101
"id": "loganalytics",
102-
"state": "stopped"
102+
"state": "stopped",
103+
"progress": [
104+
{
105+
"phase": "reindexing",
106+
"progress_percent": 0
107+
},
108+
{
109+
"phase": "loading_data",
110+
"progress_percent": 0
111+
},
112+
{
113+
"phase": "analyzing",
114+
"progress_percent": 0
115+
},
116+
{
117+
"phase": "writing_results",
118+
"progress_percent": 0
119+
}
120+
]
103121
}
104122
]
105123
}

0 commit comments

Comments
 (0)