Skip to content

Commit f5014ac

Browse files
authored
[ML] Add validation that rejects duplicate detectors in PutJobAction (#40967) (#41072)
* [ML] Add validation that rejects duplicate detectors in PutJobAction Closes #39704 * Add YML integration test for duplicate detectors fix. * Use "== false" comparison rather than "!" operator. * Refine error message to sound more natural. * Put job description in square brackets in the error message. * Use the new validation in ValidateJobConfigAction. * Exclude YML tests for new validation from permission tests.
1 parent 799541e commit f5014ac

File tree

9 files changed

+142
-14
lines changed

9 files changed

+142
-14
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PutJobAction.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ public static Request parseRequest(String jobId, XContentParser parser) {
5959
public Request(Job.Builder jobBuilder) {
6060
// Validate the jobBuilder immediately so that errors can be detected prior to transportation.
6161
jobBuilder.validateInputFields();
62+
// Validate that detector configs are unique.
63+
// This validation logically belongs to validateInputFields call but we perform it only for PUT action to avoid BWC issues which
64+
// would occur when parsing an old job config that already had duplicate detectors.
65+
jobBuilder.validateDetectorsAreUnique();
6266

6367
// Some fields cannot be set at create time
6468
List<String> invalidJobCreationSettings = jobBuilder.invalidCreateTimeSettings();

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/ValidateJobConfigAction.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,24 @@ public static class Request extends ActionRequest {
4949
private Job job;
5050

5151
public static Request parseRequest(XContentParser parser) {
52-
Job.Builder job = Job.STRICT_PARSER.apply(parser, null);
52+
Job.Builder jobBuilder = Job.STRICT_PARSER.apply(parser, null);
5353
// When jobs are PUT their ID must be supplied in the URL - assume this will
5454
// be valid unless an invalid job ID is specified in the JSON to be validated
55-
job.setId(job.getId() != null ? job.getId() : "ok");
55+
jobBuilder.setId(jobBuilder.getId() != null ? jobBuilder.getId() : "ok");
56+
57+
// Validate that detector configs are unique.
58+
// This validation logically belongs to validateInputFields call but we perform it only for PUT action to avoid BWC issues which
59+
// would occur when parsing an old job config that already had duplicate detectors.
60+
jobBuilder.validateDetectorsAreUnique();
5661

5762
// Some fields cannot be set at create time
58-
List<String> invalidJobCreationSettings = job.invalidCreateTimeSettings();
63+
List<String> invalidJobCreationSettings = jobBuilder.invalidCreateTimeSettings();
5964
if (invalidJobCreationSettings.isEmpty() == false) {
6065
throw new IllegalArgumentException(Messages.getMessage(Messages.JOB_CONFIG_INVALID_CREATE_SETTINGS,
6166
String.join(",", invalidJobCreationSettings)));
6267
}
6368

64-
return new Request(job.build(new Date()));
69+
return new Request(jobBuilder.build(new Date()));
6570
}
6671

6772
public Request() {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/Detector.java

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -489,44 +489,54 @@ public Builder(DetectorFunction function, String fieldName) {
489489
this.fieldName = fieldName;
490490
}
491491

492-
public void setDetectorDescription(String detectorDescription) {
492+
public Builder setDetectorDescription(String detectorDescription) {
493493
this.detectorDescription = detectorDescription;
494+
return this;
494495
}
495496

496-
public void setFunction(String function) {
497+
public Builder setFunction(String function) {
497498
this.function = DetectorFunction.fromString(function);
499+
return this;
498500
}
499501

500-
public void setFieldName(String fieldName) {
502+
public Builder setFieldName(String fieldName) {
501503
this.fieldName = fieldName;
504+
return this;
502505
}
503506

504-
public void setByFieldName(String byFieldName) {
507+
public Builder setByFieldName(String byFieldName) {
505508
this.byFieldName = byFieldName;
509+
return this;
506510
}
507511

508-
public void setOverFieldName(String overFieldName) {
512+
public Builder setOverFieldName(String overFieldName) {
509513
this.overFieldName = overFieldName;
514+
return this;
510515
}
511516

512-
public void setPartitionFieldName(String partitionFieldName) {
517+
public Builder setPartitionFieldName(String partitionFieldName) {
513518
this.partitionFieldName = partitionFieldName;
519+
return this;
514520
}
515521

516-
public void setUseNull(boolean useNull) {
522+
public Builder setUseNull(boolean useNull) {
517523
this.useNull = useNull;
524+
return this;
518525
}
519526

520-
public void setExcludeFrequent(ExcludeFrequent excludeFrequent) {
527+
public Builder setExcludeFrequent(ExcludeFrequent excludeFrequent) {
521528
this.excludeFrequent = excludeFrequent;
529+
return this;
522530
}
523531

524-
public void setRules(List<DetectionRule> rules) {
532+
public Builder setRules(List<DetectionRule> rules) {
525533
this.rules = rules;
534+
return this;
526535
}
527536

528-
public void setDetectorIndex(int detectorIndex) {
537+
public Builder setDetectorIndex(int detectorIndex) {
529538
this.detectorIndex = detectorIndex;
539+
return this;
530540
}
531541

532542
public Detector build() {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/Job.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,21 @@ private void validateGroups() {
10671067
}
10681068
}
10691069

1070+
/**
1071+
* Validates that the Detector configs are unique up to detectorIndex field (which is ignored).
1072+
*/
1073+
public void validateDetectorsAreUnique() {
1074+
Set<Detector> canonicalDetectors = new HashSet<>();
1075+
for (Detector detector : this.analysisConfig.getDetectors()) {
1076+
// While testing for equality, ignore detectorIndex field as this field is auto-generated.
1077+
Detector canonicalDetector = new Detector.Builder(detector).setDetectorIndex(0).build();
1078+
if (canonicalDetectors.add(canonicalDetector) == false) {
1079+
throw new IllegalArgumentException(
1080+
Messages.getMessage(Messages.JOB_CONFIG_DUPLICATE_DETECTORS_DISALLOWED, detector.getDetectorDescription()));
1081+
}
1082+
}
1083+
}
1084+
10701085
/**
10711086
* Builds a job with the given {@code createTime} and the current version.
10721087
* This should be used when a new job is created as opposed to {@link #build()}.

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ public final class Messages {
147147
public static final String JOB_CONFIG_UPDATE_ANALYSIS_LIMITS_MODEL_MEMORY_LIMIT_CANNOT_BE_DECREASED =
148148
"Invalid update value for analysis_limits: model_memory_limit cannot be decreased below current usage; " +
149149
"current usage [{0}], update had [{1}]";
150+
public static final String JOB_CONFIG_DUPLICATE_DETECTORS_DISALLOWED =
151+
"Duplicate detectors are not allowed: [{0}]";
150152
public static final String JOB_CONFIG_DETECTOR_DUPLICATE_FIELD_NAME =
151153
"{0} and {1} cannot be the same: ''{2}''";
152154
public static final String JOB_CONFIG_DETECTOR_COUNT_DISALLOWED =

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/config/JobTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,20 @@ public void testInvalidGroup_matchesJobId() {
513513
assertEquals(e.getMessage(), "job and group names must be unique but job [foo] and group [foo] have the same name");
514514
}
515515

516+
public void testInvalidAnalysisConfig_duplicateDetectors() throws Exception {
517+
Job.Builder builder =
518+
new Job.Builder("job_with_duplicate_detectors")
519+
.setCreateTime(new Date())
520+
.setDataDescription(new DataDescription.Builder())
521+
.setAnalysisConfig(new AnalysisConfig.Builder(Arrays.asList(
522+
new Detector.Builder("mean", "responsetime").build(),
523+
new Detector.Builder("mean", "responsetime").build()
524+
)));
525+
526+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, builder::validateDetectorsAreUnique);
527+
assertThat(e.getMessage(), containsString("Duplicate detectors are not allowed: [mean(responsetime)]"));
528+
}
529+
516530
public void testEarliestValidTimestamp_GivenEmptyDataCounts() {
517531
assertThat(createRandomizedJob().earliestValidTimestamp(new DataCounts("foo")), equalTo(0L));
518532
}

x-pack/plugin/ml/qa/ml-with-security/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ integTestRunner {
5757
'ml/jobs_crud/Test put job after closing state index',
5858
'ml/jobs_crud/Test put job with inconsistent body/param ids',
5959
'ml/jobs_crud/Test put job with time field in analysis_config',
60+
'ml/jobs_crud/Test put job with duplicate detector configurations',
6061
'ml/jobs_crud/Test job with categorization_analyzer and categorization_filters',
6162
'ml/jobs_get/Test get job given missing job_id',
6263
'ml/jobs_get_result_buckets/Test mutually-exclusive params',
@@ -91,6 +92,7 @@ integTestRunner {
9192
'ml/validate/Test invalid job config',
9293
'ml/validate/Test job config is invalid because model snapshot id set',
9394
'ml/validate/Test job config that is invalid only because of the job ID',
95+
'ml/validate/Test job config with duplicate detector configurations',
9496
'ml/validate_detector/Test invalid detector',
9597
'ml/delete_forecast/Test delete on _all forecasts not allow no forecasts',
9698
'ml/delete_forecast/Test delete forecast on missing forecast',

x-pack/plugin/src/test/resources/rest-api-spec/test/ml/jobs_crud.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,45 @@
10231023
"data_description" : {}
10241024
}
10251025
1026+
---
1027+
"Test put job with duplicate detector configurations":
1028+
1029+
- do:
1030+
catch: /illegal_argument_exception.*Duplicate detectors are not allowed/
1031+
ml.put_job:
1032+
job_id: jobs-crud-duplicate-detectors
1033+
body: >
1034+
{
1035+
"analysis_config": {
1036+
"bucket_span": "1h",
1037+
"detectors": [
1038+
{"function":"max", "field_name":"responsetime"},
1039+
{"function":"max", "field_name":"responsetime"}
1040+
]
1041+
},
1042+
"data_description": {
1043+
"time_field": "@timestamp"
1044+
}
1045+
}
1046+
1047+
- do:
1048+
catch: /illegal_argument_exception.*Duplicate detectors are not allowed/
1049+
ml.put_job:
1050+
job_id: jobs-crud-duplicate-detectors-with-explicit-indices
1051+
body: >
1052+
{
1053+
"analysis_config": {
1054+
"bucket_span": "1h",
1055+
"detectors": [
1056+
{"function":"max", "field_name":"responsetime", "detector_index": 0},
1057+
{"function":"max", "field_name":"responsetime", "detector_index": 1}
1058+
]
1059+
},
1060+
"data_description": {
1061+
"time_field": "@timestamp"
1062+
}
1063+
}
1064+
10261065
---
10271066
"Test put job after closing results index":
10281067

x-pack/plugin/src/test/resources/rest-api-spec/test/ml/validate.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,40 @@
106106
"data_description" : {
107107
}
108108
}
109+
110+
---
111+
"Test job config with duplicate detector configurations":
112+
113+
- do:
114+
catch: /illegal_argument_exception.*Duplicate detectors are not allowed/
115+
ml.validate:
116+
body: >
117+
{
118+
"analysis_config": {
119+
"bucket_span": "1h",
120+
"detectors": [
121+
{"function":"max", "field_name":"responsetime"},
122+
{"function":"max", "field_name":"responsetime"}
123+
]
124+
},
125+
"data_description": {
126+
"time_field": "@timestamp"
127+
}
128+
}
129+
130+
- do:
131+
catch: /illegal_argument_exception.*Duplicate detectors are not allowed/
132+
ml.validate:
133+
body: >
134+
{
135+
"analysis_config": {
136+
"bucket_span": "1h",
137+
"detectors": [
138+
{"function":"max", "field_name":"responsetime", "detector_index": 0},
139+
{"function":"max", "field_name":"responsetime", "detector_index": 1}
140+
]
141+
},
142+
"data_description": {
143+
"time_field": "@timestamp"
144+
}
145+
}

0 commit comments

Comments
 (0)